[
  {
    "path": ".coveragerc",
    "content": "[run]\ninclude =\n    tentacles/\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: OctoBot-Tentacles-CI\non:\n  push:\n    branches:\n      - 'master'\n      - 'dev'\n      - 'beta'\n    tags:\n      - '*'\n  pull_request:\n\njobs:\n  tests:\n    name: ${{ matrix.os }}${{ matrix.arch }} - Python - ${{ matrix.python }} - Tests\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [windows-latest, ubuntu-latest ]\n        arch: [ x64 ]\n        python: [ '3.10' ]\n\n    steps:\n    - uses: actions/checkout@v5\n    - name: Set up Python ${{ matrix.python }}\n      uses: actions/setup-python@v6\n      with:\n        python-version: ${{ matrix.python }}\n        architecture: ${{ matrix.arch }}\n\n    - name: Install OctoBot on Unix\n      if: matrix.os != 'windows-latest'\n      env:\n        OCTOBOT_GH_REPO: https://github.com/Drakkar-Software/OctoBot.git\n        OCTOBOT_DEFAULT_BRANCH: dev\n      run: |\n        echo \"GITHUB_REF=$GITHUB_REF\"\n        TARGET_BRANCH=$([ \"$GITHUB_HEAD_REF\" == \"\" ] && echo ${GITHUB_REF##*/} || echo \"$GITHUB_HEAD_REF\")\n        git clone -q $OCTOBOT_GH_REPO -b ${TARGET_BRANCH} || git clone -q $OCTOBOT_GH_REPO -b $OCTOBOT_DEFAULT_BRANCH\n        cd OctoBot\n        git status\n        pip install --prefer-binary -r dev_requirements.txt -r requirements.txt -r full_requirements.txt\n        cd ..\n        mkdir new_tentacles\n        cp -r Automation Backtesting Evaluator Meta Services Trading profiles new_tentacles\n        cd OctoBot\n        python start.py tentacles -d \"../new_tentacles\" -p \"../../any_platform.zip\"\n        python start.py tentacles --install --location \"../any_platform.zip\" --all\n\n    - name: Install OctoBot on Windows\n      if: matrix.os == 'windows-latest'\n      env:\n        OCTOBOT_GH_REPO: https://github.com/Drakkar-Software/OctoBot.git\n        OCTOBOT_DEFAULT_BRANCH: dev\n      run: |\n        echo \"GITHUB_REF=$env:GITHUB_REF\"\n        $env:TARGET_BRANCH = $env:GITHUB_REF\n        If ((Test-Path env:GITHUB_HEAD_REF) -and -not ([string]::IsNullOrWhiteSpace($env:GITHUB_HEAD_REF))) {\n          echo \"using GITHUB_HEAD_REF\"\n          $env:TARGET_BRANCH = $env:GITHUB_HEAD_REF\n        }\n        echo \"TARGET_BRANCH=$env:TARGET_BRANCH\"\n        If ($env:TARGET_BRANCH -notcontains \"refs/tags/\") {\n          $env:TENTACLES_URL_TAG = \"latest\"\n        }\n        echo \"cleaned TARGET_BRANCH=$env:TARGET_BRANCH\"\n        git clone -q $env:OCTOBOT_GH_REPO -b $env:TARGET_BRANCH.Replace('refs/heads/','')\n        if ($LastExitCode -ne 0) {\n          git clone -q $env:OCTOBOT_GH_REPO -b $env:OCTOBOT_DEFAULT_BRANCH\n        }\n        cd OctoBot\n        git status\n        pip install --upgrade pip setuptools wheel\n        pip install --prefer-binary -r dev_requirements.txt -r requirements.txt -r full_requirements.txt\n        cd ..\n        mkdir new_tentacles\n        xcopy Automation new_tentacles\\\\Automation /E/H/I\n        xcopy Backtesting new_tentacles\\\\Backtesting /E/H/I\n        xcopy Evaluator new_tentacles\\\\Evaluator /E/H/I\n        xcopy Meta new_tentacles\\\\Meta /E/H/I\n        xcopy Services new_tentacles\\\\Services /E/H/I\n        xcopy Trading new_tentacles\\\\Trading /E/H/I\n        xcopy profiles new_tentacles\\\\profiles /E/H/I\n        cd OctoBot\n        python start.py tentacles -d \"../new_tentacles\" -p \"../../any_platform.zip\"\n        python start.py tentacles --install --location \"../any_platform.zip\" --all\n      shell: powershell\n\n    - name: Pytests\n      run: |\n        cd OctoBot\n        pytest --cov=. --cov-config=.coveragerc --durations=0 -rw --ignore=tentacles/Trading/Exchange tentacles\n\n    - name: Publish coverage\n      if: github.event_name == 'push'\n      continue-on-error: true\n      run: coveralls\n      env:\n        COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n\n  upload_tentacles:\n    needs: tests\n    name: ${{ matrix.os }}${{ matrix.arch }} - Python - ${{ matrix.python }} - Upload\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n        arch: [ x64 ]\n        python: [ '3.10' ]\n\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set Environment Variables\n        run: |\n          echo \"S3_API_KEY=${{ secrets.S3_API_KEY }}\" >> $GITHUB_ENV\n          echo \"S3_API_SECRET_KEY=${{ secrets.S3_API_SECRET_KEY }}\" >> $GITHUB_ENV\n          echo \"S3_REGION_NAME=${{ secrets.S3_REGION_NAME }}\" >> $GITHUB_ENV\n          echo \"S3_ENDPOINT_URL=${{ secrets.S3_ENDPOINT_URL }}\" >> $GITHUB_ENV\n          echo \"CLOUDFLARE_TOKEN=${{ secrets.CLOUDFLARE_TOKEN }}\" >> $GITHUB_ENV\n          echo \"CLOUDFLARE_ZONE=${{ secrets.CLOUDFLARE_ZONE }}\" >> $GITHUB_ENV\n          TARGET_BRANCH=$([ \"$GITHUB_HEAD_REF\" == \"\" ] && echo ${GITHUB_REF##*/} || echo \"$GITHUB_HEAD_REF\")\n          echo \"TARGET_BRANCH=${TARGET_BRANCH}\" >> $GITHUB_ENV\n\n      - name: Set up Python ${{ matrix.python }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python }}\n          architecture: ${{ matrix.arch }}\n\n      - name: Produce tentacles package\n        env:\n          OCTOBOT_GH_REPO: https://github.com/Drakkar-Software/OctoBot.git\n          OCTOBOT_DEFAULT_BRANCH: dev\n        run: |\n          git clone -q $OCTOBOT_GH_REPO -b ${TARGET_BRANCH} || git clone -q $OCTOBOT_GH_REPO -b $OCTOBOT_DEFAULT_BRANCH\n          cd OctoBot\n          git status\n          pip install --prefer-binary -r dev_requirements.txt -r requirements.txt -r full_requirements.txt\n          cd ..\n          mkdir new_tentacles\n          cp -r Automation Backtesting Evaluator Meta Services Trading profiles new_tentacles\n\n      - name: Publish tag tentacles\n        if: startsWith(github.ref, 'refs/tags')\n        env:\n          S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}\n        run: |\n          sed -i \"s/VERSION_PLACEHOLDER/${TARGET_BRANCH#refs/*/}/g\" metadata.yaml\n          cd OctoBot\n          python start.py tentacles -m \"../metadata.yaml\" -d \"../new_tentacles\" -p \"../../any_platform.zip\" -ite -ute ${{ secrets.TENTACLES_OFFICIAL_PATH }}/tentacles -upe ${{ secrets.TENTACLES_OFFICIAL_PATH }}/packages/full/${{ secrets.TENTACLES_REPOSITORY_NAME }}/\n          python ../scripts/clear_cloudflare_cache.py ${TARGET_BRANCH#refs/*/}\n\n      - name: Publish latest tentacles\n        if: github.ref == 'refs/heads/dev' && startsWith(github.ref, 'refs/tags') != true\n        env:\n          S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}\n        run: |\n          sed -i \"s/VERSION_PLACEHOLDER/latest/g\" metadata.yaml\n          cd OctoBot\n          python start.py tentacles -m \"../metadata.yaml\" -d \"../new_tentacles\" -p \"../../any_platform.zip\" -upe ${{ secrets.TENTACLES_OFFICIAL_PATH }}/packages/full/${{ secrets.TENTACLES_REPOSITORY_NAME }}/\n          python ../scripts/clear_cloudflare_cache.py latest\n\n      - name: Publish stable tentacles\n        if: github.ref == 'refs/heads/master'\n        env:\n          S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}\n        run: |\n          sed -i \"s/VERSION_PLACEHOLDER/stable/g\" metadata.yaml\n          cd OctoBot\n          python start.py tentacles -m \"../metadata.yaml\" -d \"../new_tentacles\" -p \"../../any_platform.zip\" -upe ${{ secrets.TENTACLES_OFFICIAL_PATH }}/packages/full/${{ secrets.TENTACLES_REPOSITORY_NAME }}/\n          python ../scripts/clear_cloudflare_cache.py stable\n\n      - name: Publish cleaned branch tentacles\n        if: startsWith(github.ref, 'refs/tags') != true && github.ref != 'refs/heads/master'\n        env:\n          S3_BUCKET_NAME: ${{ secrets.S3_DEV_BUCKET_NAME }}\n        run: |\n          branch=\"${TARGET_BRANCH##*/}\"\n          sed -i \"s/VERSION_PLACEHOLDER/$branch/g\" metadata.yaml\n          sed -i \"s/base/$branch/g\" metadata.yaml\n          sed -i \"s/officials/dev/g\" metadata.yaml\n          cd OctoBot\n          python start.py tentacles -m \"../metadata.yaml\" -d \"../new_tentacles\" -p \"../../any_platform.zip\" -upe ${{ secrets.TENTACLES_OFFICIAL_PATH }}/packages/full/${{ secrets.TENTACLES_REPOSITORY_NAME }}/\n          python ../scripts/clear_cloudflare_cache.py $branch\n\n  notify:\n    if: ${{ failure() }}\n    needs:\n      - tests\n      - upload_tentacles\n    uses: Drakkar-Software/.github/.github/workflows/failure_notify_workflow.yml@master\n    secrets:\n      DISCORD_GITHUB_WEBHOOK: ${{ secrets.DISCORD_GITHUB_WEBHOOK }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n\\.idea/\n\n\n/__init__.py\nBacktesting/__init__.py\nBacktesting/collectors/__init__.py\nBacktesting/collectors/exchanges/__init__.py\nBacktesting/converters/__init__.py\nBacktesting/converters/exchanges/__init__.py\nBacktesting/importers/__init__.py\nBacktesting/importers/exchanges/__init__.py\nEvaluator/__init__.py\nEvaluator/RealTime/__init__.py\nEvaluator/Social/__init__.py\nEvaluator/Strategies/__init__.py\nEvaluator/TA/__init__.py\nEvaluator/Util/__init__.py\nprofiles/__init__.py\nServices/__init__.py\nServices/Interfaces/__init__.py\nServices/Notifiers/__init__.py\nServices/Services_bases/__init__.py\nServices/Services_feeds/__init__.py\nTrading/__init__.py\nTrading/Exchange/__init__.py\nTrading/Mode/__init__.py"
  },
  {
    "path": "Automation/actions/cancel_open_order_action/__init__.py",
    "content": "from .cancel_open_orders import CancelOpenOrders"
  },
  {
    "path": "Automation/actions/cancel_open_order_action/cancel_open_orders.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport asyncio\n\nimport octobot_commons.configuration as configuration\nimport octobot_trading.api as trading_api\nimport octobot.automation.bases.abstract_action as abstract_action\n\n\nclass CancelOpenOrders(abstract_action.AbstractAction):\n    async def process(self):\n        exchange_managers = trading_api.get_exchange_managers_from_exchange_ids(trading_api.get_exchange_ids())\n        await asyncio.gather(*(\n            trading_api.cancel_all_open_orders(exchange_manager)\n            for exchange_manager in exchange_managers\n        ))\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Cancel all OctoBot-managed open orders on each exchange.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {}\n\n    def apply_config(self, config):\n        # no config\n        pass\n"
  },
  {
    "path": "Automation/actions/cancel_open_order_action/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CancelOpenOrders\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/actions/sell_all_currencies_action/__init__.py",
    "content": "from .sell_all_currencies import SellAllCurrencies"
  },
  {
    "path": "Automation/actions/sell_all_currencies_action/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"SellAllCurrencies\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/actions/sell_all_currencies_action/sell_all_currencies.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport asyncio\n\nimport octobot_commons.configuration as configuration\nimport octobot_trading.api as trading_api\nimport octobot.automation.bases.abstract_action as abstract_action\n\n\nclass SellAllCurrencies(abstract_action.AbstractAction):\n    async def process(self):\n        exchange_managers = trading_api.get_exchange_managers_from_exchange_ids(trading_api.get_exchange_ids())\n        await asyncio.gather(*(\n            trading_api.sell_all_everything_for_reference_market(exchange_manager)\n            for exchange_manager in exchange_managers\n        ))\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Market sell each currency for the reference market on each exchange.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {}\n\n    def apply_config(self, config):\n        # no config\n        pass\n"
  },
  {
    "path": "Automation/actions/send_notification_action/__init__.py",
    "content": "from .send_notification import SendNotification"
  },
  {
    "path": "Automation/actions/send_notification_action/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"SendNotification\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/actions/send_notification_action/send_notification.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.configuration as configuration\nimport octobot_services.enums as services_enums\nimport octobot_services.api as services_api\nimport octobot.automation.bases.abstract_action as abstract_action\n\n\nclass SendNotification(abstract_action.AbstractAction):\n    MESSAGE = \"message\"\n\n    def __init__(self):\n        super().__init__()\n        self.notification_message = None\n\n    async def process(self):\n        await services_api.send_notification(\n            services_api.create_notification(\n                self.notification_message,\n                category=services_enums.NotificationCategory.OTHER\n            )\n        )\n\n    @staticmethod\n    def get_description() -> str:\n        return f\"Sends the configured message. \" \\\n               f\"Configure notification channels in the 'Accounts' tab. \" \\\n               f\"The notification type is '{services_enums.NotificationCategory.OTHER.value.capitalize()}'.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {\n            self.MESSAGE: UI.user_input(\n                self.MESSAGE, commons_enums.UserInputTypes.TEXT, \"Your notification triggered\", inputs,\n                title=\"Message to include in your notification.\",\n                parent_input_name=step_name,\n            )\n        }\n\n    def apply_config(self, config):\n        self.notification_message = config[self.MESSAGE]\n"
  },
  {
    "path": "Automation/actions/stop_trading_action/__init__.py",
    "content": "from .stop_trading import StopTrading"
  },
  {
    "path": "Automation/actions/stop_trading_action/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"StopTrading\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/actions/stop_trading_action/stop_trading.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport octobot_commons.constants as commons_constants\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Automation.actions.cancel_open_order_action as cancel_open_orders\n\n\nclass StopTrading(cancel_open_orders.CancelOpenOrders):\n    PROFILE_ID = commons_constants.DEFAULT_PROFILE  # non trading profile\n\n    async def process(self):\n        # cancel all open orders\n        await super().process()\n        # select non trading profile\n        config = interfaces_util.get_edited_config(dict_only=False)\n        config.select_profile(self.PROFILE_ID)\n        config.save()\n        # reboot\n        interfaces_util.get_bot_api().restart_bot()\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Cancel all OctoBot-managed open orders on each exchange, switch to the Non-Trading profile \" \\\n               \"and restart OctoBot.\"\n"
  },
  {
    "path": "Automation/conditions/no_condition_condition/__init__.py",
    "content": "from .no_condition import NoCondition"
  },
  {
    "path": "Automation/conditions/no_condition_condition/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"NoCondition\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/conditions/no_condition_condition/no_condition.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport octobot_commons.configuration as configuration\nimport octobot.automation.bases.abstract_condition as abstract_condition\n\n\nclass NoCondition(abstract_condition.AbstractCondition):\n    async def evaluate(self) -> bool:\n        return True\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Is always passing.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {}\n\n    def apply_config(self, config):\n        # no config\n        pass\n"
  },
  {
    "path": "Automation/conditions/scripted_condition/__init__.py",
    "content": "from .scripted_condition import ScriptedCondition"
  },
  {
    "path": "Automation/conditions/scripted_condition/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ScriptedCondition\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/conditions/scripted_condition/scripted_condition.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport typing\n\nimport octobot_commons.configuration as configuration\nimport octobot_trading.api as trading_api\nimport octobot_commons.enums as commons_enums\nimport octobot.automation.bases.abstract_condition as abstract_condition\nimport octobot_commons.dsl_interpreter as dsl_interpreter\n\nimport tentacles.Meta.DSL_operators as dsl_operators\n\n\nclass ScriptedCondition(abstract_condition.AbstractCondition):\n    SCRIPT = \"script\"\n    EXCHANGE = \"exchange\"\n\n    def __init__(self):\n        super().__init__()\n        self.script: str = \"\"\n        self.exchange_name: str = \"\"\n\n        self._dsl_interpreter: typing.Optional[dsl_interpreter.Interpreter] = None\n\n    async def evaluate(self) -> bool:\n        if self._dsl_interpreter:\n            script_result = await self._dsl_interpreter.interprete(self.script)\n            return bool(script_result)\n        raise ValueError(\"Scripted condition is not properly configured, the script is likely invalid.\")\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Evaluates a scripted condition using the OctoBot DSL.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        exchanges = list(trading_api.get_exchange_names())\n        return {\n            self.SCRIPT: UI.user_input(\n                self.SCRIPT, commons_enums.UserInputTypes.TEXT, \"\", inputs,\n                title=\"Scripted condition: the OctoBot DSL expression to evaluate (more info in automation details). Its return value will be converted to a boolean using \\\"bool()\\\" to determine if the condition is met.\",\n                parent_input_name=step_name,\n            ),\n            self.EXCHANGE: UI.user_input(\n                self.EXCHANGE, commons_enums.UserInputTypes.OPTIONS, exchanges[0], inputs,\n                options=exchanges,\n                title=\"Exchange: the name of the exchange to use for the condition.\",\n                parent_input_name=step_name,\n            )\n        }\n\n    def apply_config(self, config):\n        self.script = config[self.SCRIPT]\n        self.exchange_name = config[self.EXCHANGE]\n        if self.script and self.exchange_name:\n            self._dsl_interpreter = self._create_dsl_interpreter()\n            self._validate_script()\n        else:\n            self._dsl_interpreter = None\n    \n    def _validate_script(self):\n        try:\n            self._dsl_interpreter.prepare(self.script)\n            self.logger.info(\n                f\"Formula interpreter successfully prepared \\\"{self.script}\\\" condition\"\n            )\n        except Exception as e:\n            self.logger.error(f\"Error when parsing condition {self.script}: {e}\")\n            raise e\n\n    def _create_dsl_interpreter(self):\n        exchange_manager = self._get_exchange_manager()\n        ohlcv_operators = []\n        portfolio_operators = []\n        if exchange_manager is not None:\n            ohlcv_operators = dsl_operators.exchange_operators.create_ohlcv_operators(\n                exchange_manager, None, None\n            )\n            portfolio_operators = dsl_operators.exchange_operators.create_portfolio_operators(\n                exchange_manager\n            )\n        return dsl_interpreter.Interpreter(\n            dsl_interpreter.get_all_operators() + ohlcv_operators + portfolio_operators\n        )\n    \n    def _get_exchange_manager(self):\n        for exchange_id in trading_api.get_exchange_ids():\n            exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n            if exchange_manager.exchange_name == self.exchange_name and exchange_manager.is_backtesting == False:\n                return exchange_manager\n        raise ValueError(f\"No exchange manager found for exchange name: {self.exchange_name}\")\n"
  },
  {
    "path": "Automation/trigger_events/period_check_event/__init__.py",
    "content": "from .period_check import PeriodicCheck"
  },
  {
    "path": "Automation/trigger_events/period_check_event/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"PeriodicCheck\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/trigger_events/period_check_event/period_check.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport asyncio\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.configuration as configuration\nimport octobot.automation.bases.abstract_trigger_event as abstract_trigger_event\n\n\nclass PeriodicCheck(abstract_trigger_event.AbstractTriggerEvent):\n    UPDATE_PERIOD = \"update_period\"\n\n    def __init__(self):\n        super().__init__()\n        self.waiter_task = None\n        self.waiting_time = None\n\n    async def stop(self):\n        await super().stop()\n        if self.waiter_task is not None and not self.waiter_task.done():\n            self.waiter_task.cancel()\n\n    async def _get_next_event(self):\n        if self.should_stop:\n            raise StopIteration\n        self.waiter_task = asyncio.create_task(asyncio.sleep(self.waiting_time))\n        await self.waiter_task\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Will trigger periodically, at the specified update period.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {\n            self.UPDATE_PERIOD: UI.user_input(\n                self.UPDATE_PERIOD, commons_enums.UserInputTypes.FLOAT, 300, inputs,\n                title=\"Update period: number of seconds to wait between each update.\",\n                parent_input_name=step_name,\n            )\n        }\n\n    def apply_config(self, config):\n        self.waiting_time = config[self.UPDATE_PERIOD]\n"
  },
  {
    "path": "Automation/trigger_events/price_threshold_event/__init__.py",
    "content": "from .price_threshold import PriceThreshold"
  },
  {
    "path": "Automation/trigger_events/price_threshold_event/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"PriceThreshold\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/trigger_events/price_threshold_event/price_threshold.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport asyncio\nimport decimal\n\nimport async_channel.enums as channel_enums\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.configuration as configuration\nimport octobot_commons.channels_name as channels_name\nimport octobot.automation.bases.abstract_trigger_event as abstract_trigger_event\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.api as trading_api\n\n\nclass PriceThreshold(abstract_trigger_event.AbstractTriggerEvent):\n    TARGET_PRICE = \"target_price\"\n    SYMBOL = \"symbol\"\n    TRIGGER_ONLY_ONCE = \"trigger_only_once\"\n    MAX_TRIGGER_FREQUENCY = \"max_trigger_frequency\"\n\n    def __init__(self):\n        super().__init__()\n        self.waiter_task = None\n        self.symbol = None\n        self.target_price = None\n        self.last_price = None\n        self.trigger_event = asyncio.Event()\n        self.registered_consumer = False\n        self.consumers = []\n\n    async def _register_consumer(self):\n        self.registered_consumer = True\n        for exchange_id in trading_api.get_exchange_ids():\n            self.consumers.append(\n                await exchanges_channel.get_chan(\n                    channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value,\n                    exchange_id\n                ).new_consumer(\n                    self.mark_price_callback,\n                    priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value,\n                    symbol=self.symbol\n                )\n            )\n\n    async def mark_price_callback(\n            self, exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mark_price\n    ):\n        if self.should_stop:\n            # do not go any further if the action has been stopped\n            return\n        self._check_threshold(mark_price)\n        self._update_last_price(mark_price)\n\n    def _update_last_price(self, mark_price):\n        self.last_price = mark_price\n\n    def _check_threshold(self, mark_price):\n        if self.last_price is None:\n            return\n        if mark_price >= self.target_price > self.last_price or mark_price <= self.target_price < self.last_price:\n            # mark_price crossed self.target_price threshold\n            self.trigger_event.set()\n\n    async def stop(self):\n        await super().stop()\n        if self.waiter_task is not None and not self.waiter_task.done():\n            self.waiter_task.cancel()\n        for consumer in self.consumers:\n            await consumer.stop()\n        self.consumers = []\n\n    async def _get_next_event(self):\n        if self.should_stop:\n            raise StopIteration\n        if not self.registered_consumer:\n            await self._register_consumer()\n        self.waiter_task = asyncio.create_task(asyncio.wait_for(self.trigger_event.wait(), timeout=None))\n        await self.waiter_task\n        self.trigger_event.clear()\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Will trigger when the price of the given symbol crosses the given price.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {\n            self.SYMBOL: UI.user_input(\n                self.SYMBOL, commons_enums.UserInputTypes.TEXT, \"BTC/USDT\", inputs,\n                title=\"Symbol: symbol to watch price on. Example: ETH/BTC or BTC/USDT:USDT\",\n                parent_input_name=step_name,\n            ),\n            self.TARGET_PRICE: UI.user_input(\n                self.TARGET_PRICE, commons_enums.UserInputTypes.FLOAT, 300, inputs,\n                title=\"Target price: price triggering the event.\",\n                parent_input_name=step_name,\n            ),\n            self.MAX_TRIGGER_FREQUENCY: UI.user_input(\n                self.MAX_TRIGGER_FREQUENCY, commons_enums.UserInputTypes.FLOAT, 0.0, inputs,\n                title=\"Maximum trigger frequency: required time between each trigger. In seconds. \"\n                      \"Useful to avoid spamming in certain situations.\",\n                parent_input_name=step_name,\n            ),\n            self.TRIGGER_ONLY_ONCE: UI.user_input(\n                self.TRIGGER_ONLY_ONCE, commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n                title=\"Trigger only once: can only trigger once until OctoBot restart or \"\n                      \"the automation configuration changes.\",\n                parent_input_name=step_name,\n            ),\n        }\n\n    def apply_config(self, config):\n        self.trigger_event.clear()\n        self.last_price = None\n        self.symbol = config[self.SYMBOL]\n        self.target_price = decimal.Decimal(str(config[self.TARGET_PRICE]))\n        self.trigger_only_once = config[self.TRIGGER_ONLY_ONCE]\n        self.max_trigger_frequency = config[self.MAX_TRIGGER_FREQUENCY]\n"
  },
  {
    "path": "Automation/trigger_events/profitability_threshold_event/__init__.py",
    "content": "from .profitability_threshold import ProfitabilityThreshold"
  },
  {
    "path": "Automation/trigger_events/profitability_threshold_event/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ProfitabilityThreshold\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Automation/trigger_events/profitability_threshold_event/profitability_threshold.py",
    "content": "#  This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)\n#  Copyright (c) 2023 Drakkar-Software, All rights reserved.\n#\n#  OctoBot is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  OctoBot is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public\n#  License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.\nimport asyncio\nimport decimal\nimport time\nimport sortedcontainers\n\nimport async_channel.enums as channel_enums\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.configuration as configuration\nimport octobot_commons.channels_name as channels_name\nimport octobot.automation.bases.abstract_trigger_event as abstract_trigger_event\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.api as trading_api\nimport octobot_trading.constants as trading_constants\n\n\nclass ProfitabilityThreshold(abstract_trigger_event.AbstractTriggerEvent):\n    PERCENT_CHANGE = \"percent_change\"\n    TIME_PERIOD = \"time_period\"\n    TRIGGER_ONLY_ONCE = \"trigger_only_once\"\n    MAX_TRIGGER_FREQUENCY = \"max_trigger_frequency\"\n\n    def __init__(self):\n        super().__init__()\n        self.waiter_task = None\n        self.percent_change = None\n        self.time_period = None\n        self.profitability_by_time = None\n        self.trigger_event = asyncio.Event()\n        self.registered_consumer = False\n        self.consumers = []\n\n    async def _register_consumer(self):\n        self.registered_consumer = True\n        for exchange_id in trading_api.get_exchange_ids():\n            self.consumers.append(\n                await exchanges_channel.get_chan(\n                    channels_name.OctoBotTradingChannelsName.BALANCE_PROFITABILITY_CHANNEL.value,\n                    exchange_id\n                ).new_consumer(\n                    self.balance_profitability_callback,\n                    priority_level=channel_enums.ChannelConsumerPriorityLevels.MEDIUM.value\n                )\n            )\n\n    async def balance_profitability_callback(\n            self,\n            exchange: str,\n            exchange_id: str,\n            profitability,\n            profitability_percent,\n            market_profitability_percent,\n            initial_portfolio_current_profitability,\n    ):\n        if self.should_stop:\n            # do not go any further if the action has been stopped\n            return\n        self._update_profitability_by_time(profitability_percent)\n        self._check_threshold(profitability_percent)\n\n    def _update_profitability_by_time(self, profitability_percent):\n        self.profitability_by_time[int(time.time())] = profitability_percent\n        current_time = time.time()\n        for profitability_time in list(self.profitability_by_time):\n            if profitability_time - current_time > self.time_period:\n                self.profitability_by_time.pop(profitability_time)\n\n    def _check_threshold(self, profitability_percent):\n        oldest_compared_profitability = next(iter(self.profitability_by_time.values()))\n        if trading_constants.ZERO < self.percent_change <= profitability_percent - oldest_compared_profitability:\n            # profitability_percent reached or when above self.percent_change\n            self.trigger_event.set()\n        if trading_constants.ZERO > self.percent_change >= profitability_percent - oldest_compared_profitability:\n            # profitability_percent reached or when bellow self.percent_change\n            self.trigger_event.set()\n\n    async def stop(self):\n        await super().stop()\n        if self.waiter_task is not None and not self.waiter_task.done():\n            self.waiter_task.cancel()\n        for consumer in self.consumers:\n            await consumer.stop()\n        self.consumers = []\n\n    async def _get_next_event(self):\n        if self.should_stop:\n            raise StopIteration\n        if not self.registered_consumer:\n            await self._register_consumer()\n        self.waiter_task = asyncio.create_task(asyncio.wait_for(self.trigger_event.wait(), timeout=None))\n        await self.waiter_task\n        self.trigger_event.clear()\n\n    @staticmethod\n    def get_description() -> str:\n        return \"Will trigger when profitability reaches the given % change on the given time window. \" \\\n               \"Example: a Percent change of 10 will trigger the automation if your OctoBot profitability \" \\\n               \"changes from 0 to 10 or from 30 to 40.\"\n\n    def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:\n        return {\n            self.PERCENT_CHANGE: UI.user_input(\n                self.PERCENT_CHANGE, commons_enums.UserInputTypes.FLOAT, 35, inputs,\n                title=\"Percent change: minimum change of % profitability to trigger the automation. \"\n                      \"Can be negative to trigger on losses.\",\n                parent_input_name=step_name,\n            ),\n            self.TIME_PERIOD: UI.user_input(\n                self.TIME_PERIOD, commons_enums.UserInputTypes.FLOAT, 300, inputs,\n                title=\"Time period: maximum time to consider to compute profitability changes. In minutes.\",\n                parent_input_name=step_name,\n            ),\n            self.MAX_TRIGGER_FREQUENCY: UI.user_input(\n                self.MAX_TRIGGER_FREQUENCY, commons_enums.UserInputTypes.FLOAT, 0.0, inputs,\n                title=\"Maximum trigger frequency: required time between each trigger. In seconds. \"\n                      \"Useful to avoid spamming in certain situations.\",\n                parent_input_name=step_name,\n            ),\n            self.TRIGGER_ONLY_ONCE: UI.user_input(\n                self.TRIGGER_ONLY_ONCE, commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n                title=\"Trigger only once: can only trigger once until OctoBot restart or \"\n                      \"the automation configuration changes.\",\n                parent_input_name=step_name,\n            ),\n        }\n\n    def apply_config(self, config):\n        self.trigger_event.clear()\n        self.profitability_by_time = sortedcontainers.SortedDict()\n        self.percent_change = decimal.Decimal(str(config[self.PERCENT_CHANGE]))\n        self.time_period = config[self.TIME_PERIOD] * commons_constants.MINUTE_TO_SECONDS\n        self.trigger_only_once = config[self.TRIGGER_ONLY_ONCE]\n        self.max_trigger_frequency = config[self.MAX_TRIGGER_FREQUENCY]\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_bot_snapshot_data_collector/__init__.py",
    "content": "from .bot_snapshot_with_history_collector import ExchangeBotSnapshotWithHistoryCollector"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_bot_snapshot_data_collector/bot_snapshot_with_history_collector.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport copy\nimport os\nimport json\nimport time\nimport shutil\nimport collections\n\nimport octobot_backtesting.collectors as collector\nimport octobot_backtesting.importers as importers\nimport octobot_backtesting.enums as backtesting_enums\nimport octobot_backtesting.constants as backtesting_constants\nimport octobot_backtesting.errors as backtesting_errors\nimport octobot_commons.errors as commons_errors\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.databases as databases\nimport octobot_backtesting.data as data\nimport octobot_trading.api as trading_api\nimport octobot_trading.errors as trading_errors\nimport tentacles.Backtesting.importers.exchanges.generic_exchange_importer as generic_exchange_importer\n\n\n\nclass ExchangeBotSnapshotWithHistoryCollector(collector.AbstractExchangeBotSnapshotCollector):\n    IMPORTER = generic_exchange_importer.GenericExchangeDataImporter\n    OHLCV = \"ohlcv\"\n    KLINE = \"kline\"\n\n    def __init__(self, config, exchange_name, exchange_type, tentacles_setup_config, symbols, time_frames,\n                 use_all_available_timeframes=False,\n                 data_format=backtesting_enums.DataFormats.REGULAR_COLLECTOR_DATA,\n                 start_timestamp=None,\n                 end_timestamp=None):\n        super().__init__(config, exchange_name, exchange_type, tentacles_setup_config, symbols, time_frames,\n                         use_all_available_timeframes, data_format=data_format,\n                         start_timestamp=start_timestamp, end_timestamp=end_timestamp)\n        self.exchange_type = None\n        self.exchange_manager = None\n        self.fetch_exchange_manager = None\n        self.file_name = data.get_backtesting_file_name(self.__class__,\n                                                        self.get_permanent_file_identifier,\n                                                        data_format=data_format)\n        self.is_creating_database = False\n        self.description = None\n        self.missing_symbols = []\n        self.fetched_data = {\n            self.OHLCV: {},\n            self.KLINE: {},\n        }\n        self.set_file_path()\n\n    def get_permanent_file_identifier(self):\n        symbols = \"-\".join(symbol_util.merge_symbol(symbol.symbol_str) for symbol in self.symbols)\n        time_frames = \"-\".join(tf.value for tf in self.time_frames)\n        return f\"{self.exchange_name}{backtesting_constants.BACKTESTING_DATA_FILE_SEPARATOR}\" \\\n               f\"{symbols}{backtesting_constants.BACKTESTING_DATA_FILE_SEPARATOR}{time_frames}\"\n\n    async def initialize(self):\n        self.create_database()\n        await self.database.initialize()\n        await self._check_database_content()\n\n    def set_file_path(self) -> None:\n        super().set_file_path()\n        if os.path.isfile(self.file_path):\n            shutil.copy(self.file_path, self.temp_file_path)\n\n    def finalize_database(self):\n        if os.path.isfile(self.file_path):\n            os.remove(self.file_path)\n        os.rename(self.temp_file_path, self.file_path)\n\n    async def _check_database_content(self):\n        # load description\n        try:\n            self.description = await data.get_database_description(self.database)\n            found_exchange_name = self.description[backtesting_enums.DataFormatKeys.EXCHANGE.value]\n            found_symbols = [symbol_util.parse_symbol(symbol)\n                             for symbol in self.description[backtesting_enums.DataFormatKeys.SYMBOLS.value]]\n            found_time_frames = self.description[backtesting_enums.DataFormatKeys.TIME_FRAMES.value]\n            if found_exchange_name != self.exchange_name:\n                raise backtesting_errors.IncompatibleDatafileError(f\"Exchange name in database: {found_exchange_name}, \"\n                                                                   f\"requested exchange: {self.exchange_name}\")\n            if found_symbols != self.symbols:\n                raise backtesting_errors.IncompatibleDatafileError(f\"Pairs in database: {found_symbols}, \"\n                                                                   f\"requested exchange: {self.symbols}\")\n            if found_time_frames != self.time_frames:\n                raise backtesting_errors.IncompatibleDatafileError(f\"Time frames name in database: {found_time_frames}, \"\n                                                                   f\"requested exchange: {self.time_frames}\")\n        except commons_errors.DatabaseNotFoundError:\n            # newly created datafile\n            self.is_creating_database = True\n\n    async def start(self):\n        self.should_stop = False\n        should_stop_database = True\n        self.current_step_percent = 0\n        self.total_steps = len(self.time_frames) * len(self.symbols)\n        try:\n            self.exchange_manager = trading_api.get_exchange_manager_from_exchange_id(self.exchange_id)\n\n            # use a secondary exchange manager to fetch candles to fix ccxt pagination issues\n            # seen on ccxt 4.1.82\n            other_config = copy.copy(self.config)\n            other_config[commons_constants.CONFIG_TIME_FRAME] = []   # any value here to avoid crashing\n            self.fetch_exchange_manager = await trading_api.create_exchange_builder(other_config, self.exchange_name) \\\n                .is_simulated() \\\n                .is_rest_only() \\\n                .is_exchange_only() \\\n                .is_future(self.exchange_manager.is_future) \\\n                .disable_trading_mode() \\\n                .use_tentacles_setup_config(self.tentacles_setup_config) \\\n                .build()\n\n            await self.adapt_timestamps()\n\n            # create/update description\n            if self.is_creating_database:\n                await self._create_description()\n            else:\n                await self._update_description()\n\n            self.in_progress = True\n\n            self.logger.info(f\"Start collecting history on {self.exchange_name}\")\n            tasks = []\n            for symbol_index, symbol in enumerate(self.symbols):\n                if symbol in self.missing_symbols:\n                    self.logger.error(f\"Skipping {symbol} from backtesting data: \"\n                                      f\"missing price history on {self.exchange_name}\")\n                    continue\n                self.logger.info(f\"Collecting history for {symbol}...\")\n                tasks.append(asyncio.create_task(self.get_ticker_history(self.exchange_name, symbol)))\n                tasks.append(asyncio.create_task(self.get_order_book_history(self.exchange_name, symbol)))\n                tasks.append(asyncio.create_task(self.get_recent_trades_history(self.exchange_name, symbol)))\n\n                for time_frame_index, time_frame in enumerate(self.time_frames):\n                    tasks.append(asyncio.create_task(self.get_ohlcv_history(self.exchange_name, symbol, time_frame)))\n                    tasks.append(asyncio.create_task(self.get_kline_history(self.exchange_name, symbol, time_frame)))\n                    if symbol_index == time_frame_index == 0:\n                        # let tables get created\n                        await asyncio.gather(*tasks)\n                        tasks = []\n            if tasks:\n                await asyncio.gather(*tasks)\n\n        except Exception as err:\n            await self.database.stop()\n            should_stop_database = False\n            # Do not keep errored data file\n            if os.path.isfile(self.temp_file_path):\n                os.remove(self.temp_file_path)\n            if not self.should_stop:\n                self.logger.exception(err, True, f\"Error when collecting {self.exchange_name} history for \"\n                                                 f\"{', '.join([symbol.symbol_str for symbol in self.symbols])}: {err}\")\n                raise backtesting_errors.DataCollectorError(err) from err\n        finally:\n            await self.stop(should_stop_database=should_stop_database)\n\n    async def stop(self, should_stop_database=True):\n        self.should_stop = True\n        if should_stop_database:\n            await self.database.stop()\n            self.finalize_database()\n        await self.fetch_exchange_manager.stop()\n        self.exchange_manager = None\n        self.in_progress = False\n        self.finished = True\n        return self.finished\n\n    async def _update_description(self):\n        updated_values = {}\n        if self.end_timestamp and int(self.description[backtesting_enums.DataFormatKeys.END_TIMESTAMP.value]) * 1000 < self.end_timestamp:\n            updated_values[\"end_timestamp\"] = int(self.end_timestamp/1000)\n        if self.start_timestamp and int(self.description[backtesting_enums.DataFormatKeys.START_TIMESTAMP.value]) * 1000 > self.start_timestamp:\n            updated_values[\"start_timestamp\"] = int(self.start_timestamp/1000)\n        if updated_values:\n            updated_values[\"timestamp\"] = time.time()\n            await self.database.update(backtesting_enums.DataTables.DESCRIPTION,\n                                       updated_value_by_column=updated_values,\n                                       version=self.VERSION,\n                                       exchange=self.exchange_name,\n                                       symbols=json.dumps([symbol.symbol_str for symbol in self.symbols]),\n                                       time_frames=json.dumps([tf.value for tf in self.time_frames]))\n\n    async def get_ticker_history(self, exchange, symbol):\n        pass\n\n    async def get_order_book_history(self, exchange, symbol):\n        pass\n\n    async def get_recent_trades_history(self, exchange, symbol):\n        pass\n\n    def get_ohlcv_snapshot(self, symbol, time_frame):\n        symbol_data = trading_api.get_symbol_data(self.exchange_manager, str(symbol), allow_creation=False)\n        candles = trading_api.get_symbol_historical_candles(symbol_data, time_frame)\n        return [\n            [\n                time_val,\n                candles[commons_enums.PriceIndexes.IND_PRICE_OPEN.value][index],\n                candles[commons_enums.PriceIndexes.IND_PRICE_HIGH.value][index],\n                candles[commons_enums.PriceIndexes.IND_PRICE_LOW.value][index],\n                candles[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value][index],\n                candles[commons_enums.PriceIndexes.IND_PRICE_VOL.value][index],\n            ]\n            for index, time_val in enumerate(candles[commons_enums.PriceIndexes.IND_PRICE_TIME.value])\n        ]\n\n    async def collect_historical_ohlcv(self, exchange, symbol, time_frame, time_frame_sec,\n                                       start_time, end_time, progress_multiplier):\n        last_progress = 0\n        symbol_id = str(symbol)\n        async for candles in trading_api.get_historical_ohlcv(\n            self.fetch_exchange_manager, symbol_id, time_frame, start_time, end_time\n        ):\n            await self.save_ohlcv(\n                    exchange=exchange,\n                    cryptocurrency=self.exchange_manager.exchange.get_pair_cryptocurrency(symbol_id),\n                    symbol=symbol.symbol_str, time_frame=time_frame, candle=candles,\n                    timestamp=[candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] + time_frame_sec\n                               for candle in candles],\n                    multiple=True\n            )\n            progress = (candles[-1][commons_enums.PriceIndexes.IND_PRICE_TIME.value] - self.start_timestamp / 1000) / \\\n                                        ((self.end_timestamp - self.start_timestamp) / 1000) * 100\n            progress_over_all_steps = progress * progress_multiplier / self.total_steps\n            self.current_step_percent += progress_over_all_steps - last_progress\n            self.logger.debug(f\"progress: {self.current_step_percent}%\")\n            last_progress = progress_over_all_steps\n        return last_progress\n\n    def find_candle(self, candles, timestamp):\n        for candle in candles:\n            if candle[-1][commons_enums.PriceIndexes.IND_PRICE_TIME.value] == timestamp:\n                return candle[-1], candle[0]\n        return None, None\n\n    async def update_ohlcv(self, exchange, symbol, time_frame, time_frame_sec,\n                           database_candles, current_bot_candles):\n        to_add_candles = []\n        symbol_id = str(symbol)\n        for up_to_date_candle in current_bot_candles:\n            current_candle_time = up_to_date_candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n            equivalent_db_candle, candle_timestamp = self.find_candle(database_candles, current_candle_time)\n            if equivalent_db_candle is None:\n                to_add_candles.append(up_to_date_candle)\n            elif equivalent_db_candle != up_to_date_candle:\n                updated_value_by_column = {\n                    \"candle\": json.dumps(up_to_date_candle)\n                }\n                await self.database.update(backtesting_enums.ExchangeDataTables.OHLCV,\n                                           updated_value_by_column=updated_value_by_column,\n                                           exchange_name=exchange,\n                                           cryptocurrency=\n                                           self.exchange_manager.exchange.get_pair_cryptocurrency(symbol_id),\n                                           symbol=symbol.symbol_str,\n                                           time_frame=time_frame.value,\n                                           timestamp=str(candle_timestamp))\n        if to_add_candles:\n            await self.save_ohlcv(\n                exchange=exchange,\n                cryptocurrency=self.exchange_manager.exchange.get_pair_cryptocurrency(symbol_id),\n                symbol=symbol, time_frame=time_frame, candle=to_add_candles,\n                timestamp=[candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] + time_frame_sec\n                           for candle in to_add_candles],\n                multiple=True\n            )\n\n    async def _check_ohlcv_integrity(self, database_candles):\n        # ensure no timestamp is here twice\n        all_timestamps = [candle[-1][0] for candle in database_candles]\n        unique_timestamps = set(all_timestamps)\n        if len(unique_timestamps) != len(database_candles):\n            return {\n                timestamp: counter\n                for timestamp, counter in collections.Counter(all_timestamps).items()\n                if counter > 1\n            }\n        return {}\n\n    async def get_ohlcv_history(self, exchange, symbol, time_frame):\n        try:\n            last_progress = 0\n            time_frame_sec = commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MINUTE_TO_SECONDS\n            # use current data from current bot\n            fetch_data_id = self.get_fetch_data_id(symbol, time_frame)\n            already_fetched_candles_candles = self.fetched_data[self.OHLCV][fetch_data_id]\n            database_candles = []\n            save_all_candles = self.is_creating_database\n            updated_db = False\n            if not self.is_creating_database:\n                database_candles = await self._import_candles_from_datafile(exchange, symbol, time_frame)\n                counters = await self._check_ohlcv_integrity(database_candles)\n                if counters:\n                    self.logger.warning(f\"Duplicate candles in {exchange} data file for {symbol.symbol_str} \"\n                                        f\"on {time_frame}. Problematic timestamps: {counters}. \"\n                                        f\"Resetting database to ensure data integrity\")\n\n                    await self.delete_all(\n                        backtesting_enums.ExchangeDataTables.OHLCV,\n                        exchange=exchange,\n                        cryptocurrency=self.exchange_manager.exchange.get_pair_cryptocurrency(str(symbol)),\n                        symbol=symbol.symbol_str,\n                        time_frame=time_frame\n                    )\n                    updated_db = True\n                    save_all_candles = True\n            if save_all_candles or not database_candles:\n                await self.save_ohlcv(\n                        exchange=exchange,\n                        cryptocurrency=self.exchange_manager.exchange.get_pair_cryptocurrency(str(symbol)),\n                        symbol=symbol.symbol_str, time_frame=time_frame, candle=already_fetched_candles_candles,\n                        timestamp=[candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] + time_frame_sec\n                                   for candle in already_fetched_candles_candles],\n                        multiple=True\n                )\n                database_candles = await self._import_candles_from_datafile(exchange, symbol, time_frame)\n                updated_db = True\n            candle_times = [\n                candle[-1][commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n                for candle in database_candles\n            ]\n            # +/-1 not to fetch the last candle twice\n            first_candle_data_time = min(candle_times) * 1000 - 1\n            last_candle_data_time = max(candle_times) * 1000 + 1\n            fill_before = self.start_timestamp and self.start_timestamp + time_frame_sec * 1000 < first_candle_data_time\n            fill_after = last_candle_data_time < self.end_timestamp\n            progress_per_collect = 0.5 if fill_after and fill_before else 1\n            # 1. fill in any missing candle before existing candles\n            if fill_before:\n                # fetch missing data between required start time and actual start time in data file\n                last_progress = await self.collect_historical_ohlcv(\n                    exchange, symbol, time_frame, time_frame_sec, self.start_timestamp, first_candle_data_time,\n                    progress_per_collect\n                )\n                if last_progress:\n                    self.current_step_percent += 100 * progress_per_collect / self.total_steps - last_progress\n                    updated_db = True\n            # 2. fill in any missing candle after existing candles\n            if fill_after:\n                # fetch missing data between end time in data file and available data\n                last_progress = await self.collect_historical_ohlcv(\n                    exchange, symbol, time_frame, time_frame_sec, last_candle_data_time, self.end_timestamp,\n                    progress_per_collect\n                )\n                if last_progress:\n                    self.current_step_percent += 100 * progress_per_collect / self.total_steps - last_progress\n                    updated_db = True\n            if not (fill_before or fill_after):\n                # nothing to collect, update progress still\n                self.current_step_percent += 100 / self.total_steps\n            if updated_db:\n                database_candles = await self._import_candles_from_datafile(exchange, symbol, time_frame)\n                counters = await self._check_ohlcv_integrity(database_candles)\n                if counters:\n                    self.logger.error(f\"Error when checking database integrity of {exchange} \"\n                                      f\"data file for {symbol.symbol_str}. \"\n                                      f\"Delete this data file: {self.file_name} to reset it. \"\n                                      f\"Problematic timestamps: {counters}\")\n        except Exception:\n            raise\n\n    async def _import_candles_from_datafile(self, exchange, symbol, time_frame):\n        return importers.import_ohlcvs(\n            await self.database.select(backtesting_enums.ExchangeDataTables.OHLCV,\n                                       size=databases.SQLiteDatabase.DEFAULT_SIZE,\n                                       exchange_name=exchange, symbol=symbol.symbol_str,\n                                       time_frame=time_frame.value)\n        )\n\n    async def get_kline_history(self, exchange, symbol, time_frame):\n        pass\n\n    async def adapt_timestamps(self):\n        lowest_timestamps = []\n        for symbol in self.symbols:\n            for tf in self.time_frames:\n                first_timestamp = await self.get_first_candle_timestamp(\n                    self.start_timestamp, symbol, tf\n                )\n                if first_timestamp is None:\n                    self.missing_symbols.append(symbol)\n                    break\n                else:\n                    lowest_timestamps.append(first_timestamp)\n        lowest_timestamp = min(lowest_timestamps)\n        # lowest_timestamp depends on self.start_timestamp if set. It will not go further\n        if self.start_timestamp is None or lowest_timestamp < self.start_timestamp:\n            self.start_timestamp = lowest_timestamp\n        self.end_timestamp = self.end_timestamp or time.time() * 1000\n        if self.start_timestamp > self.end_timestamp:\n            raise backtesting_errors.DataCollectorError(\"start_timestamp is higher than end_timestamp\")\n\n    def get_fetch_data_id(self, symbol, timeframe):\n        return f\"{symbol}{timeframe.value}\"\n\n    async def get_first_candle_timestamp(self, ideal_start_timestamp, symbol, time_frame):\n        try:\n            symbol_data = trading_api.get_symbol_data(self.exchange_manager, str(symbol), allow_creation=False)\n            candles = trading_api.get_symbol_historical_candles(symbol_data, time_frame)\n            self.fetched_data[self.OHLCV][self.get_fetch_data_id(symbol, time_frame)] = self.get_ohlcv_snapshot(\n                symbol, time_frame\n            )\n            return candles[commons_enums.PriceIndexes.IND_PRICE_TIME.value][0] * 1000\n        except KeyError:\n            # symbol or timeframe not available in live exchange\n            fetched_candles = await self.fetch_exchange_manager.exchange.get_symbol_prices(\n                str(symbol), time_frame, limit=1, since=ideal_start_timestamp\n            )\n            if not fetched_candles:\n                return None\n            self.fetched_data[self.OHLCV][self.get_fetch_data_id(symbol, time_frame)] = fetched_candles\n            return fetched_candles[0][commons_enums.PriceIndexes.IND_PRICE_TIME.value] * 1000\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_bot_snapshot_data_collector/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ExchangeBotSnapshotCollector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_history_collector/__init__.py",
    "content": "from .history_collector import ExchangeHistoryDataCollector"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_history_collector/history_collector.pxd",
    "content": "# cython: language_level=3\n#  Drakkar-Software OctoBot-Backtesting\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom octobot_backtesting.collectors.exchanges.exchange_collector cimport AbstractExchangeHistoryCollector\n\ncdef class ExchangeHistoryDataCollector(AbstractExchangeHistoryCollector):\n    cdef public object exchange\n    cdef public object exchange_manager\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_history_collector/history_collector.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport logging\nimport os\nimport time\n\nimport octobot_backtesting.collectors as collector\nimport octobot_backtesting.enums as backtesting_enums\nimport octobot_backtesting.errors as errors\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport tentacles.Backtesting.importers.exchanges.generic_exchange_importer as generic_exchange_importer\n\ntry:\n    import octobot_trading.api as trading_api\n    import octobot_trading.enums as trading_enums\n    import octobot_trading.errors as trading_errors\nexcept ImportError:\n    logging.error(\"ExchangeHistoryDataCollector requires OctoBot-Trading package installed\")\n\n\nclass ExchangeHistoryDataCollector(collector.AbstractExchangeHistoryCollector):\n    IMPORTER = generic_exchange_importer.GenericExchangeDataImporter\n\n    def __init__(self, config, exchange_name, exchange_type, tentacles_setup_config, symbols, time_frames,\n                 use_all_available_timeframes=False,\n                 data_format=backtesting_enums.DataFormats.REGULAR_COLLECTOR_DATA,\n                 start_timestamp=None,\n                 end_timestamp=None):\n        super().__init__(config, exchange_name, exchange_type, tentacles_setup_config, symbols, time_frames,\n                         use_all_available_timeframes, data_format=data_format,\n                         start_timestamp=start_timestamp, end_timestamp=end_timestamp)\n        self.exchange = None\n        self.exchange_manager = None\n\n    async def start(self):\n        self.should_stop = False\n        should_stop_database = True\n        try:\n            use_future = self.exchange_type == trading_enums.ExchangeTypes.FUTURE\n            self.exchange_manager = await trading_api.create_exchange_builder(self.config, self.exchange_name) \\\n                .is_simulated() \\\n                .is_rest_only() \\\n                .is_exchange_only() \\\n                .is_future(use_future) \\\n                .disable_trading_mode() \\\n                .use_tentacles_setup_config(self.tentacles_setup_config) \\\n                .build()\n\n            self.exchange = self.exchange_manager.exchange\n            self._load_timeframes_if_necessary()\n\n            await self.check_timestamps()\n\n            # create description\n            await self._create_description()\n\n            self.total_steps = len(self.time_frames) * len(self.symbols)\n            self.in_progress = True\n\n            self.logger.info(f\"Start collecting history on {self.exchange_name}\")\n            for symbol_index, symbol in enumerate(self.symbols):\n                self.logger.info(f\"Collecting history for {symbol}...\")\n                await self.get_ticker_history(self.exchange_name, symbol)\n                await self.get_order_book_history(self.exchange_name, symbol)\n                await self.get_recent_trades_history(self.exchange_name, symbol)\n\n                for time_frame_index, time_frame in enumerate(self.time_frames):\n                    self.current_step_index = (symbol_index * len(self.time_frames)) + time_frame_index + 1\n                    self.logger.info(\n                        f\"[{time_frame_index}/{len(self.time_frames)}] Collecting {symbol} history on {time_frame}...\")\n                    await self.get_ohlcv_history(self.exchange_name, symbol, time_frame)\n                    await self.get_kline_history(self.exchange_name, symbol, time_frame)\n        except Exception as err:\n            await self.database.stop()\n            should_stop_database = False\n            # Do not keep errored data file\n            if os.path.isfile(self.temp_file_path):\n                os.remove(self.temp_file_path)\n            if not self.should_stop:\n                self.logger.exception(err, True, f\"Error when collecting {self.exchange_name} history for \"\n                                                 f\"{', '.join([str(symbol) for symbol in self.symbols])}: {err}\")\n                raise errors.DataCollectorError(err)\n        finally:\n            await self.stop(should_stop_database=should_stop_database)\n\n    def _load_all_available_timeframes(self):\n        allowed_timeframes = set(tf.value for tf in commons_enums.TimeFrames)\n        self.time_frames = [commons_enums.TimeFrames(time_frame)\n                            for time_frame in self.exchange_manager.client_time_frames\n                            if time_frame in allowed_timeframes]\n\n    async def stop(self, should_stop_database=True):\n        self.should_stop = True\n        if self.exchange_manager is not None:\n            await self.exchange_manager.stop()\n        if should_stop_database:\n            await self.database.stop()\n            self.finalize_database()\n        self.exchange_manager = None\n        self.in_progress = False\n        self.finished = True\n        return self.finished\n\n    async def get_ticker_history(self, exchange, symbol):\n        pass\n\n    async def get_order_book_history(self, exchange, symbol):\n        pass\n\n    async def get_recent_trades_history(self, exchange, symbol):\n        pass\n\n    async def get_ohlcv_history(self, exchange, symbol, time_frame):\n        self.current_step_percent = 0\n        # use time_frame_sec to add time to save the candle closing time\n        time_frame_sec = commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MINUTE_TO_SECONDS\n        symbol_id = str(symbol)\n        cryptocurrency = self.exchange_manager.exchange.get_pair_cryptocurrency(symbol_id)\n        if self.start_timestamp is not None:\n            start_time = self.start_timestamp\n            end_time = self.end_timestamp or time.time() * 1000\n            first_candle_timestamp = await self.get_first_candle_timestamp(\n                self.start_timestamp, symbol, time_frame\n            ) * 1000\n            if self.start_timestamp < first_candle_timestamp:\n                start_time = first_candle_timestamp\n            async for hist_candles in trading_api.get_historical_ohlcv(self.exchange_manager, symbol_id, time_frame,\n                                                                       start_time, end_time):\n                if hist_candles:\n                    self.current_step_percent = \\\n                        (hist_candles[-1][commons_enums.PriceIndexes.IND_PRICE_TIME.value] - start_time / 1000) / \\\n                        ((end_time - start_time) / 1000) * 100\n                    self.logger.info(f\"[{self.current_step_percent}%] historical data fetched for {symbol} {time_frame}\")\n                    await self.save_ohlcv(\n                        exchange=exchange,\n                        cryptocurrency=cryptocurrency,\n                        symbol=symbol.symbol_str, time_frame=time_frame, candle=hist_candles,\n                        timestamp=[candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] + time_frame_sec\n                                   for candle in hist_candles],\n                        multiple=True)\n        else:\n            try:\n                candles = await self.exchange.get_symbol_prices(symbol_id, time_frame)\n                if candles:\n                    await self.save_ohlcv(exchange=exchange,\n                                          cryptocurrency=cryptocurrency,\n                                          symbol=symbol.symbol_str, time_frame=time_frame, candle=candles,\n                                          timestamp=[candle[0] + time_frame_sec for candle in candles], multiple=True)\n                else:\n                    self.logger.error(f\"No candles for {symbol} on {time_frame} ({exchange})\")\n            except trading_errors.FailedRequest as err:\n                self.logger.exception(err, False)\n                self.logger.warning(f\"Ignored {symbol} {time_frame} candles on {exchange} ({err})\")\n\n    async def get_kline_history(self, exchange, symbol, time_frame):\n        pass\n\n    async def check_timestamps(self):\n        if self.start_timestamp is not None:\n            lowest_timestamp = min([\n                await self.get_first_candle_timestamp(\n                    self.start_timestamp, symbol, time_frame_manager.find_min_time_frame(self.time_frames)\n                )\n                for symbol in self.symbols\n            ])\n            if lowest_timestamp > self.start_timestamp:\n                self.start_timestamp = lowest_timestamp\n            if self.start_timestamp > (self.end_timestamp if self.end_timestamp else (time.time() * 1000)):\n                raise errors.DataCollectorError(\"start_timestamp is higher than end_timestamp\")\n\n    async def get_first_candle_timestamp(self, ideal_start_timestamp, symbol, time_frame):\n        try:\n            return (\n                await self.exchange.get_symbol_prices(str(symbol), time_frame, limit=1, since=ideal_start_timestamp)\n            )[0][commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n        except (trading_errors.FailedRequest, IndexError) as err:\n            raise errors.DataCollectorError(\n                f\"Impossible to initialize {self.exchange_name} data collector: {err}. This means that {symbol} \"\n                f\"for the {time_frame.value} time frame is not supported in this context on {self.exchange_name}.\"\n            )\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_history_collector/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ExchangeHistoryDataCollector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_history_collector/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_history_collector/tests/test_history_collector.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport os\nimport contextlib\nimport json\nimport asyncio\n\nimport octobot_commons.databases as databases\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_backtesting.enums as enums\nimport octobot_backtesting.errors as errors\nimport octobot_trading.enums as trading_enums\nimport tests.test_utils.config as test_utils_config\nimport tentacles.Backtesting.collectors.exchanges as collector_exchanges\nimport tentacles.Trading.Exchange as tentacles_exchanges\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\nBINANCEUS = \"binanceus\"\nBINANCEUS_MAX_CANDLES_COUNT = 500\n\n\n@contextlib.asynccontextmanager\nasync def data_collector(exchange_name, tentacles_setup_config, symbols, time_frames, use_all_available_timeframes,\n                         start_timestamp=None, end_timestamp=None):\n    collector_instance = collector_exchanges.ExchangeHistoryDataCollector(\n        {}, exchange_name, trading_enums.ExchangeTypes.SPOT, tentacles_setup_config,\n        [commons_symbols.parse_symbol(symbol) for symbol in symbols], time_frames,\n        use_all_available_timeframes=use_all_available_timeframes,\n        start_timestamp=start_timestamp,\n        end_timestamp=end_timestamp\n    )\n    try:\n        await collector_instance.initialize()\n        yield collector_instance\n    finally:\n        if collector_instance.file_path and os.path.isfile(collector_instance.file_path):\n            os.remove(collector_instance.file_path)\n        if collector_instance.temp_file_path and os.path.isfile(collector_instance.temp_file_path):\n            os.remove(collector_instance.temp_file_path)\n\n\n@contextlib.asynccontextmanager\nasync def collector_database(collector):\n    database = databases.SQLiteDatabase(collector.file_path)\n    try:\n        await database.initialize()\n        yield database\n    finally:\n        await database.stop()\n\n\nasync def test_collect_valid_data():\n    tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n    symbols = [\"ETH/BTC\"]\n    async with data_collector(BINANCEUS, tentacles_setup_config, symbols, None, True) as collector:\n        assert collector.time_frames == []\n        assert collector.symbols == [commons_symbols.parse_symbol(symbol) for symbol in symbols]\n        assert collector.exchange_name == BINANCEUS\n        assert collector.tentacles_setup_config == tentacles_setup_config\n        await collector.start()\n        assert collector.time_frames != []\n        assert collector.exchange_manager is None\n        assert isinstance(collector.exchange, tentacles_exchanges.BinanceUS)\n        assert collector.file_path is not None\n        assert collector.temp_file_path is not None\n        assert not os.path.isfile(collector.temp_file_path)\n        assert os.path.isfile(collector.file_path)\n        async with collector_database(collector) as database:\n            ohlcv = await database.select(enums.ExchangeDataTables.OHLCV)\n            # use > to take into account new possible candles since collect max time is not specified\n            assert len(ohlcv) > 6000\n            h_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, time_frame=\"1h\")\n            assert len(h_ohlcv) == BINANCEUS_MAX_CANDLES_COUNT\n            eth_btc_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, symbol=\"ETH/BTC\")\n            assert len(eth_btc_ohlcv) == len(ohlcv)\n\n\nasync def test_collect_invalid_data():\n    tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n    symbols = [\"___ETH/BTC\"]\n    async with data_collector(BINANCEUS, tentacles_setup_config, symbols, None, True) as collector:\n        with pytest.raises(errors.DataCollectorError):\n            await collector.start()\n        assert collector.time_frames != []\n        assert collector.exchange_manager is None\n        assert collector.exchange is not None\n        assert collector.file_path is not None\n        assert collector.temp_file_path is not None\n        assert not os.path.isfile(collector.temp_file_path)\n\n\nasync def test_collect_valid_date_range():\n    tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n    symbols = [\"ETH/BTC\"]\n    start_time = 1569413160000\n    end_time = 1569914160000\n    # each request fetches 500 candles\n    candle_fetch_limit = 500\n    async with data_collector(BINANCEUS, tentacles_setup_config, symbols, None, True, start_time,\n                              end_time) as collector:\n        assert collector.start_timestamp is not None\n        assert collector.end_timestamp is not None\n        await collector.start()\n        assert collector.time_frames != []\n        assert collector.exchange_manager is None\n        assert isinstance(collector.exchange, tentacles_exchanges.BinanceUS)\n        assert collector.file_path is not None\n        assert collector.temp_file_path is not None\n        assert os.path.isfile(collector.file_path)\n        assert not os.path.isfile(collector.temp_file_path)\n        async with collector_database(collector) as database:\n            ohlcv = await database.select(enums.ExchangeDataTables.OHLCV)\n            assert len(ohlcv) == 13943\n            parsed_candles = [\n                json.loads(candle[-1])\n                for candle in ohlcv\n            ]\n            for parsed_candle in parsed_candles:\n                candle_open_time = parsed_candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n                assert start_time <= candle_open_time * 1000 <= end_time\n            for time_frame in commons_enums.TimeFrames:\n                time_frame_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, time_frame=time_frame.value)\n                if not time_frame_ohlcv:\n                    continue\n                all_timestamps = sorted([\n                    candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n                    for candle in (\n                        json.loads(candle[-1])\n                        for candle in time_frame_ohlcv\n                    )\n                ])\n                # ensure no duplicate\n                timestamps = set(all_timestamps)\n                assert len(timestamps) == len(time_frame_ohlcv)\n                # ensure no missing\n                interval = commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MINUTE_TO_SECONDS\n                current_ts = all_timestamps[0] - interval\n                for timestamp in all_timestamps:\n                    current_ts += interval\n                    assert timestamp == current_ts\n\n            h_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV,\n                                            time_frame=commons_enums.TimeFrames.ONE_HOUR.value)\n            assert len(h_ohlcv) == 139\n            eth_btc_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, symbol=\"ETH/BTC\")\n            assert len(eth_btc_ohlcv) == len(ohlcv)\n            min_timestamp = (await database.select_min(enums.ExchangeDataTables.OHLCV, [\"timestamp\"],\n                                                       time_frame=commons_enums.TimeFrames.ONE_MINUTE.value))[0][\n                                commons_enums.PriceIndexes.IND_PRICE_TIME.value] * 1000\n            assert start_time <= min_timestamp <= start_time + (60 * 1000)\n            max_timestamp = (await database.select_max(enums.ExchangeDataTables.OHLCV, [\"timestamp\"]))[0][\n                                commons_enums.PriceIndexes.IND_PRICE_TIME.value] * 1000\n            assert end_time <= max_timestamp <= end_time + (31 * 24 * 60 * 60 * 1000)\n\n\nasync def test_collect_invalid_date_range():\n    tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n    symbols = [\"ETH/BTC\"]\n    async with data_collector(BINANCEUS, tentacles_setup_config, symbols, None, True, 1609459200, 1577836800) \\\n            as collector:\n        assert collector.start_timestamp is not None\n        assert collector.end_timestamp is not None\n        with pytest.raises(errors.DataCollectorError):\n            await collector.start()\n        assert collector.time_frames != []\n        assert collector.exchange_manager is None\n        assert isinstance(collector.exchange, tentacles_exchanges.BinanceUS)\n        assert collector.file_path is not None\n        assert collector.temp_file_path is not None\n        assert not os.path.isfile(collector.file_path)\n        assert not os.path.isfile(collector.temp_file_path)\n\n\nasync def test_collect_multi_pair():\n    tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n    symbols = [\"ETH/BTC\", \"BTC/USDT\", \"LTC/BTC\"]\n    async with data_collector(BINANCEUS, tentacles_setup_config, symbols, None, True) as collector:\n        assert collector.time_frames == []\n        assert collector.symbols == [commons_symbols.parse_symbol(symbol) for symbol in symbols]\n        assert collector.exchange_name == BINANCEUS\n        assert collector.tentacles_setup_config == tentacles_setup_config\n        await collector.start()\n        assert collector.time_frames != []\n        assert collector.exchange_manager is None\n        assert isinstance(collector.exchange, tentacles_exchanges.BinanceUS)\n        assert collector.file_path is not None\n        assert collector.temp_file_path is not None\n        assert not os.path.isfile(collector.temp_file_path)\n        assert os.path.isfile(collector.file_path)\n        async with collector_database(collector) as database:\n            ohlcv = await database.select(enums.ExchangeDataTables.OHLCV)\n            # use > to take into account new possible candles since collect max time is not specified\n            assert len(ohlcv) > 19316\n            h_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, time_frame=\"4h\")\n            assert len(h_ohlcv) == len(symbols) * BINANCEUS_MAX_CANDLES_COUNT\n            symbols_description = json.loads((await database.select(enums.DataTables.DESCRIPTION))[0][3])\n            assert all(symbol in symbols_description for symbol in symbols)\n            eth_btc_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, symbol=\"ETH/BTC\")\n            assert len(eth_btc_ohlcv) > 6598\n            inch_btc_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, symbol=\"LTC/BTC\")\n            assert len(inch_btc_ohlcv) > 5803\n            btc_usdt_ohlcv = await database.select(enums.ExchangeDataTables.OHLCV, symbol=\"BTC/USDT\")\n            assert len(btc_usdt_ohlcv) > 6598\n\n\nasync def test_stop_collect():\n    tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n    symbols = [\"AAVE/USDT\"]\n    async with data_collector(BINANCEUS, tentacles_setup_config, symbols, None, True, 1549065660000,\n                              1632090006000) as collector:\n        async def stop_soon():\n            await asyncio.sleep(5)\n            await collector.stop(should_stop_database=False)\n\n        await asyncio.gather(collector.start(), stop_soon())\n        assert collector.time_frames != []\n        assert collector.symbols == [commons_symbols.parse_symbol(symbol) for symbol in symbols]\n        assert collector.exchange_name == BINANCEUS\n        assert collector.tentacles_setup_config == tentacles_setup_config\n        assert collector.finished\n        assert collector.exchange_manager is None\n        assert not os.path.isfile(collector.temp_file_path)\n        assert not os.path.isfile(collector.file_path)\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_live_collector/__init__.py",
    "content": "from .live_collector import ExchangeLiveDataCollector"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_live_collector/live_collector.pxd",
    "content": "# cython: language_level=3\n#  Drakkar-Software OctoBot-Backtesting\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom octobot_backtesting.collectors.exchanges.exchange_collector cimport ExchangeDataCollector\n\ncdef class ExchangeLiveDataCollector(ExchangeDataCollector):\n    pass\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_live_collector/live_collector.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport logging\nimport time\n\nimport octobot_backtesting.collectors.exchanges as exchanges\nimport octobot_commons.channels_name as channels_name\nimport tentacles.Backtesting.importers.exchanges.generic_exchange_importer as generic_exchange_importer\n\ntry:\n    import octobot_trading.exchange_channel as exchange_channel\n    import octobot_trading.api as trading_api\nexcept ImportError:\n    logging.error(\"ExchangeLiveDataCollector requires OctoBot-Trading package installed\")\n\n\nclass ExchangeLiveDataCollector(exchanges.AbstractExchangeLiveCollector):\n    IMPORTER = generic_exchange_importer.GenericExchangeDataImporter\n\n    async def start(self):\n        exchange_manager = await trading_api.create_exchange_builder(self.config, self.exchange_name) \\\n            .is_simulated() \\\n            .is_rest_only() \\\n            .is_without_auth() \\\n            .is_ignoring_config() \\\n            .disable_trading_mode() \\\n            .use_tentacles_setup_config(self.tentacles_setup_config) \\\n            .build()\n\n        self._load_timeframes_if_necessary()\n\n        # create description\n        await self._create_description()\n\n        exchange_id = exchange_manager.id\n        await exchange_channel.get_chan(channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value,\n                                        exchange_id).new_consumer(self.ticker_callback)\n        await exchange_channel.get_chan(channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value,\n                                        exchange_id).new_consumer(self.recent_trades_callback)\n        await exchange_channel.get_chan(channels_name.OctoBotTradingChannelsName.ORDER_BOOK_CHANNEL.value,\n                                        exchange_id).new_consumer(self.order_book_callback)\n        await exchange_channel.get_chan(channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,\n                                        exchange_id).new_consumer(self.kline_callback)\n        await exchange_channel.get_chan(channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,\n                                        exchange_id).new_consumer(self.ohlcv_callback)\n\n        await asyncio.gather(*asyncio.all_tasks(asyncio.get_event_loop()))\n\n    async def ticker_callback(self, exchange: str, exchange_id: str,\n                              cryptocurrency: str, symbol: str, ticker):\n        self.logger.info(f\"TICKER : CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} || TICKER = {ticker}\")\n        await self.save_ticker(timestamp=time.time(), exchange=exchange,\n                               cryptocurrency=cryptocurrency, symbol=symbol, ticker=ticker)\n\n    async def order_book_callback(self, exchange: str, exchange_id: str,\n                                  cryptocurrency: str, symbol: str, asks, bids):\n        self.logger.info(f\"ORDERBOOK : CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} \"\n                         f\"|| ASKS = {asks} || BIDS = {bids}\")\n        await self.save_order_book(timestamp=time.time(), exchange=exchange,\n                                   cryptocurrency=cryptocurrency, symbol=symbol, asks=asks, bids=bids)\n\n    async def recent_trades_callback(self, exchange: str, exchange_id: str,\n                                     cryptocurrency: str, symbol: str, recent_trades):\n        self.logger.info(f\"RECENT TRADE : CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} \"\n                         f\"|| RECENT TRADE = {recent_trades}\")\n        await self.save_recent_trades(timestamp=time.time(), exchange=exchange,\n                                      cryptocurrency=cryptocurrency, symbol=symbol, recent_trades=recent_trades)\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle):\n        self.logger.info(f\"OHLCV : CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} \"\n                         f\"|| TIME FRAME = {time_frame} || CANDLE = {candle}\")\n        await self.save_ohlcv(timestamp=time.time(), exchange=exchange,\n                              cryptocurrency=cryptocurrency, symbol=symbol, time_frame=time_frame, candle=candle)\n\n    async def kline_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, kline):\n        self.logger.info(f\"KLINE : CRYPTOCURRENCY = {cryptocurrency} || SYMBOL = {symbol} \"\n                         f\"|| TIME FRAME = {time_frame} || KLINE = {kline}\")\n        await self.save_kline(timestamp=time.time(), exchange=exchange,\n                              cryptocurrency=cryptocurrency, symbol=symbol, time_frame=time_frame, kline=kline)\n"
  },
  {
    "path": "Backtesting/collectors/exchanges/exchange_live_collector/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ExchangeLiveDataCollector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Backtesting/converters/exchanges/legacy_data_converter/__init__.py",
    "content": "from .legacy_converter import LegacyDataConverter"
  },
  {
    "path": "Backtesting/converters/exchanges/legacy_data_converter/legacy_converter.pxd",
    "content": "# cython: language_level=3\n#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom octobot_backtesting.converters.data_converter cimport DataConverter\nfrom octobot_backtesting.data.database cimport DataBase\n\ncdef class LegacyDataConverter(DataConverter):\n    cdef str exchange_name\n    cdef str symbol\n    cdef str time_data\n    cdef list time_frames\n    cdef dict file_content\n    cdef DataBase database\n\n    cdef list _get_formatted_candles(self, object time_frame)\n    cdef dict _read_data_file(self)\n    cdef dict _read_data_file(self)\n"
  },
  {
    "path": "Backtesting/converters/exchanges/legacy_data_converter/legacy_converter.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport gzip\nimport json\nimport enum\nimport os.path as path\nimport datetime\n\nimport octobot_backtesting.collectors.exchanges as exchanges\nimport octobot_backtesting.constants as backtesting_constants\nimport octobot_backtesting.converters as converters\nimport octobot_backtesting.data as backtesting_data\nimport octobot_backtesting.enums as backtesting_enums\nimport octobot_commons.databases as databases\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.symbols.symbol_util as symbol_util\n\n\nclass LegacyDataConverter(converters.DataConverter):\n    \"\"\"\n    LegacyDataConverter can be used to convert OctoBot v0.3 data files into v0.4 data files.\n    \"\"\"\n    DATA_FILE_EXT = \".data\"\n    VERSION = \"1.0\"\n    DATA_FILE_TIME_DATE_FORMAT = '%Y%m%d%H%M%S'\n\n    class PriceIndexes(enum.Enum):\n        IND_PRICE_TIME = 0\n        IND_PRICE_OPEN = 1\n        IND_PRICE_HIGH = 2\n        IND_PRICE_LOW = 3\n        IND_PRICE_CLOSE = 4\n        IND_PRICE_VOL = 5\n\n    def __init__(self, backtesting_file_to_convert):\n        super().__init__(backtesting_file_to_convert)\n        self.exchange_name = \"\"\n        self.symbol = \"\"\n        self.time_data = \"\"\n        self.time_frames = []\n        self.file_content = {}\n        self.database = None\n        self.converted_file = backtesting_data.get_backtesting_file_name(exchanges.AbstractExchangeHistoryCollector)\n\n    async def can_convert(self, ) -> bool:\n        self.exchange_name, self.symbol, self.time_data = LegacyDataConverter._interpret_file_name(self.file_to_convert)\n        if None in (self.exchange_name, self.symbol, self.time_data):\n            return False\n        self.file_content = self._read_data_file()\n        if not self.file_content:\n            return False\n        for time_frame, candles_data in self.file_content.items():\n            try:\n                # check time frame validity\n                time_frame = commons_enums.TimeFrames(time_frame)\n                # check candle data validity\n                if isinstance(candles_data, list) and len(candles_data) == 6:\n                    # check candle data non-emptiness\n                    if all(data for data in candles_data):\n                        self.time_frames.append(time_frame)\n            except ValueError:\n                pass\n        return bool(self.time_frames)\n\n    async def convert(self) -> bool:\n        try:\n            self.database = databases.SQLiteDatabase(\n                path.join(backtesting_constants.BACKTESTING_FILE_PATH, self.converted_file))\n            await self.database.initialize()\n            await self._create_description()\n            for time_frame in self.time_frames:\n                await self._convert_ohlcv(time_frame)\n            return True\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error while converting data file: {e}\")\n            return False\n        finally:\n            if self.database is not None:\n                await self.database.stop()\n\n    async def _create_description(self):\n        time_object = datetime.datetime.strptime(self.time_data, self.DATA_FILE_TIME_DATE_FORMAT)\n        await self.database.insert(backtesting_enums.DataTables.DESCRIPTION,\n                                   timestamp=datetime.datetime.timestamp(time_object),\n                                   version=self.VERSION,\n                                   exchange=self.exchange_name,\n                                   symbols=json.dumps([self.symbol]),\n                                   time_frames=json.dumps([tf.value for tf in self.time_frames]))\n\n    async def _convert_ohlcv(self, time_frame):\n        # use time_frame_sec to add time to save the candle closing time\n        time_frame_sec = commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MINUTE_TO_SECONDS\n        candles = self._get_formatted_candles(time_frame)\n        await self.database.insert_all(backtesting_enums.ExchangeDataTables.OHLCV,\n                                       timestamp=[candle[0] + time_frame_sec for candle in candles],\n                                       exchange_name=self.exchange_name, symbol=self.symbol,\n                                       time_frame=time_frame.value, candle=[json.dumps(c) for c in candles])\n\n    def _get_formatted_candles(self, time_frame):\n        data = self.file_content[time_frame.value]\n        candles = []\n        for i in range(len(data[LegacyDataConverter.PriceIndexes.IND_PRICE_TIME.value])):\n            candles.insert(i, [None] * len(LegacyDataConverter.PriceIndexes))\n            candles[i][LegacyDataConverter.PriceIndexes.IND_PRICE_CLOSE.value] = \\\n                data[LegacyDataConverter.PriceIndexes.IND_PRICE_CLOSE.value][i]\n            candles[i][LegacyDataConverter.PriceIndexes.IND_PRICE_OPEN.value] = \\\n                data[LegacyDataConverter.PriceIndexes.IND_PRICE_OPEN.value][i]\n            candles[i][LegacyDataConverter.PriceIndexes.IND_PRICE_HIGH.value] = \\\n                data[LegacyDataConverter.PriceIndexes.IND_PRICE_HIGH.value][i]\n            candles[i][LegacyDataConverter.PriceIndexes.IND_PRICE_LOW.value] = \\\n                data[LegacyDataConverter.PriceIndexes.IND_PRICE_LOW.value][i]\n            candles[i][LegacyDataConverter.PriceIndexes.IND_PRICE_TIME.value] = \\\n                data[LegacyDataConverter.PriceIndexes.IND_PRICE_TIME.value][i]\n            candles[i][LegacyDataConverter.PriceIndexes.IND_PRICE_VOL.value] = \\\n                data[LegacyDataConverter.PriceIndexes.IND_PRICE_VOL.value][i]\n        return candles\n\n    def _read_data_file(self):\n        try:\n            # try zipfile\n            with gzip.open(self.file_to_convert, 'r') as file_to_parse:\n                file_content = json.loads(file_to_parse.read())\n        except OSError:\n            # try without unzip\n            with open(self.file_to_convert) as file_to_parse:\n                file_content = json.loads(file_to_parse.read())\n        except Exception:\n            return {}\n        return file_content\n\n    @staticmethod\n    def _interpret_file_name(file_name):\n        data = path.basename(file_name).split(\"_\")\n        try:\n            exchange_name = data[0]\n            symbol = symbol_util.merge_currencies(data[1], data[2])\n            file_ext = LegacyDataConverter.DATA_FILE_EXT\n            timestamp = data[3] + data[4].replace(file_ext, \"\")\n        except KeyError:\n            exchange_name = None\n            symbol = None\n            timestamp = None\n\n        return exchange_name, symbol, timestamp\n"
  },
  {
    "path": "Backtesting/converters/exchanges/legacy_data_converter/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"LegacyDataConverter\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Backtesting/importers/exchanges/generic_exchange_importer/__init__.py",
    "content": "from .generic_exchange_importer import GenericExchangeDataImporter"
  },
  {
    "path": "Backtesting/importers/exchanges/generic_exchange_importer/generic_exchange_importer.pxd",
    "content": "# cython: language_level=3\n#  Drakkar-Software OctoBot-Backtesting\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom octobot_backtesting.importers.exchanges.exchange_importer cimport ExchangeDataImporter\n\ncdef class GenericExchangeDataImporter(ExchangeDataImporter):\n    pass"
  },
  {
    "path": "Backtesting/importers/exchanges/generic_exchange_importer/generic_exchange_importer.py",
    "content": "#  Drakkar-Software OctoBot-Backtesting\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_backtesting.importers as importers\n\n\nclass GenericExchangeDataImporter(importers.ExchangeDataImporter):\n    pass\n"
  },
  {
    "path": "Backtesting/importers/exchanges/generic_exchange_importer/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GenericExchangeDataImporter\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/__init__.py",
    "content": "from .instant_fluctuations import InstantFluctuationsEvaluator, InstantMAEvaluator"
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/config/InstantFluctuationsEvaluator.json",
    "content": "{\n    \"price_difference_threshold_percent\": 1,\n    \"volume_difference_threshold_percent\": 400,\n    \"time_frame\": \"1m\"\n}"
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/config/InstantMAEvaluator.json",
    "content": "{\n    \"period\": 6,\n    \"time_frame\": \"1m\",\n    \"threshold\": 0.5\n}"
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/instant_fluctuations.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport math\nimport tulipy\nimport numpy as np\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.channels_name as channels_name\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.util as evaluators_util\n\n\nclass InstantFluctuationsEvaluator(evaluators.RealTimeEvaluator):\n    \"\"\"\n    Idea: moves are lasting approx 12min\n    Check the last 12 candles and compute mean closing prices as\n    well as mean volume with a gradually narrower interval to\n    compute the strength or weakness of the move\n    \"\"\"\n\n    PRICE_THRESHOLD_KEY = \"price_difference_threshold_percent\"\n    VOLUME_THRESHOLD_KEY = \"volume_difference_threshold_percent\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.something_is_happening = False\n        self.last_notification_eval = 0\n\n        self.average_prices = {}\n        self.last_price = 0\n\n        # Volume\n        self.average_volumes = {}\n        self.last_volume = 0\n\n        # Constants\n        self.time_frame = None\n        self.VOLUME_HAPPENING_THRESHOLD = None\n        self.PRICE_HAPPENING_THRESHOLD = None\n        self.MIN_TRIGGERING_DELTA = 0.15\n        self.candle_segments = [10, 8, 6, 5, 4, 3, 2, 1]\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.time_frame = self.time_frame or \\\n            self.UI.user_input(commons_constants.CONFIG_TIME_FRAME, commons_enums.UserInputTypes.OPTIONS,\n                               commons_enums.TimeFrames.ONE_MINUTE.value,\n                               inputs, options=[tf.value for tf in commons_enums.TimeFrames],\n                               title=\"Time frame: The time frame to observe in order to spot changes.\")\n        self.VOLUME_HAPPENING_THRESHOLD = 1 + self.UI.user_input(\n            self.VOLUME_THRESHOLD_KEY, commons_enums.UserInputTypes.FLOAT, 400, inputs, min_val=0,\n            title=\"Volume threshold: volume difference in percent from which to trigger a notification.\"\n        ) / 100\n        self.PRICE_HAPPENING_THRESHOLD = self.UI.user_input(\n            self.PRICE_THRESHOLD_KEY, commons_enums.UserInputTypes.FLOAT, 1, inputs, min_val=0,\n            title=\"Price threshold: price difference in percent from which to trigger a notification.\"\n        ) / 100\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle):\n        volume_data = self.get_symbol_candles(exchange, exchange_id, symbol, time_frame). \\\n            get_symbol_volume_candles(self.candle_segments[0])\n        close_data = self.get_symbol_candles(exchange, exchange_id, symbol, time_frame). \\\n            get_symbol_close_candles(self.candle_segments[0])\n        for segment in self.candle_segments:\n            volume_data = [d for d in volume_data[-segment:] if d is not None]\n            price_data = [d for d in close_data[-segment:] if d is not None]\n            self.average_volumes[segment] = np.mean(volume_data)\n            self.average_prices[segment] = np.mean(price_data)\n\n        try:\n            self.last_volume = volume_data[-1]\n            self.last_price = close_data[-1]\n            await self._trigger_evaluation(cryptocurrency, symbol,\n                                           evaluators_util.get_eval_time(full_candle=candle, time_frame=time_frame))\n        except IndexError:\n            # candles data history is probably not yet available\n            self.logger.debug(f\"Impossible to evaluate, no historical data for {symbol} on {time_frame}\")\n\n    async def kline_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, kline):\n        self.last_volume = kline[commons_enums.PriceIndexes.IND_PRICE_VOL.value]\n        self.last_price = kline[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value]\n        await self._trigger_evaluation(cryptocurrency, symbol, evaluators_util.get_eval_time(kline=kline))\n\n    async def _trigger_evaluation(self, cryptocurrency, symbol, time):\n        self.evaluate_volume_fluctuations()\n        if self.something_is_happening and self.eval_note != commons_constants.START_PENDING_EVAL_NOTE:\n            if abs(self.last_notification_eval - self.eval_note) >= self.MIN_TRIGGERING_DELTA:\n                self.last_notification_eval = self.eval_note\n                await self.evaluation_completed(cryptocurrency, symbol, self.available_time_frame,\n                                                eval_time=time)\n            self.something_is_happening = False\n        else:\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n\n    def evaluate_volume_fluctuations(self):\n        volume_trigger = 0\n        price_trigger = 0\n\n        for segment in self.candle_segments:\n            if segment in self.average_volumes and segment in self.average_prices:\n                # check volume fluctuation\n                if self.last_volume > self.VOLUME_HAPPENING_THRESHOLD * self.average_volumes[segment]:\n                    volume_trigger += 1\n                    self.something_is_happening = True\n\n                # check price fluctuation\n                segment_average_price = self.average_prices[segment]\n                if self.last_price > (1 + self.PRICE_HAPPENING_THRESHOLD) * segment_average_price:\n                    price_trigger += 1\n                    self.something_is_happening = True\n\n                elif self.last_price < (1 - self.PRICE_HAPPENING_THRESHOLD) * segment_average_price:\n                    price_trigger -= 1\n                    self.something_is_happening = True\n\n        if self.candle_segments:\n            average_volume_trigger = min(1, volume_trigger / len(self.candle_segments) + 0.2)\n            average_price_trigger = price_trigger / len(self.candle_segments)\n\n            if average_price_trigger < 0:\n                # math.cos(1-x) between 0 and 1 starts around 0.5 and smoothly goes up to 1\n                self.eval_note = -1 * math.cos(1 - (-1 * average_price_trigger * average_volume_trigger))\n            elif average_price_trigger > 0:\n                self.eval_note = math.cos(1 - average_price_trigger * average_volume_trigger)\n            else:\n                # no price info => high volume but no price move, can't say anything\n                self.something_is_happening = False\n        else:\n            self.something_is_happening = False\n\n    async def start(self, bot_id: str) -> bool:\n        \"\"\"\n        Subscribe to Kline and OHLCV notification\n        :return: bool\n        \"\"\"\n        try:\n            import octobot_trading.exchange_channel as exchange_channels\n            import octobot_trading.api as trading_api\n            exchange_id = trading_api.get_exchange_id_from_matrix_id(self.exchange_name, self.matrix_id)\n            await exchange_channels.get_chan(channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,\n                                             exchange_id).new_consumer(\n                callback=self.ohlcv_callback, symbol=self.symbol,\n                time_frame=self.available_time_frame, priority_level=self.priority_level)\n            await exchange_channels.get_chan(channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,\n                                             exchange_id).new_consumer(\n                callback=self.kline_callback, symbol=self.symbol,\n                time_frame=self.available_time_frame, priority_level=self.priority_level)\n            return True\n        except ImportError:\n            self.logger.error(\"Can't connect to trading channels\")\n        return False\n\n    def set_default_config(self):\n        super().set_default_config()\n        self.specific_config[commons_constants.CONFIG_TIME_FRAME] = \"1m\"\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not symbol dependant else False\n        \"\"\"\n        return False\n\n\nclass InstantMAEvaluator(evaluators.RealTimeEvaluator):\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.last_candle_data = {}\n        self.last_moving_average_values = {}\n        self.period = 6\n        self.time_frame = None\n        self.price_threshold = 0.05\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.time_frame = self.time_frame or \\\n            self.UI.user_input(commons_constants.CONFIG_TIME_FRAME, commons_enums.UserInputTypes.OPTIONS,\n                               commons_enums.TimeFrames.ONE_MINUTE.value,\n                               inputs, options=[tf.value for tf in commons_enums.TimeFrames],\n                               title=\"Time frame: The time frame to observe in order to spot changes.\")\n        self.period = self.UI.user_input(\"period\", commons_enums.UserInputTypes.INT, 6, inputs,\n                                         min_val=1, title=\"Period: the EMA period length to use.\")\n        self.price_threshold = self.UI.user_input(\n            \"threshold\", commons_enums.UserInputTypes.FLOAT, self.price_threshold * 100, inputs, min_val=0,\n            title=\"Price threshold: price difference in percent from the current moving average value starting \"\n                  \"from which to trigger an evaluation.\"\n        ) / 100\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle):\n        self.eval_note = 0\n        new_data = self.get_symbol_candles(exchange, exchange_id, symbol, time_frame). \\\n            get_symbol_close_candles(20)\n        should_eval = symbol not in self.last_candle_data or \\\n                      not self._compare_data(new_data, self.last_candle_data[symbol])\n        self.last_candle_data[symbol] = new_data\n        if should_eval:\n            if len(self.last_candle_data[symbol]) > self.period:\n                self.last_moving_average_values[symbol] = tulipy.sma(self.last_candle_data[symbol],\n                                                                     self.period)\n                await self._evaluate_current_price(self.last_candle_data[symbol][-1], cryptocurrency, symbol,\n                                                   evaluators_util.get_eval_time(full_candle=candle,\n                                                                                 time_frame=time_frame))\n\n    async def kline_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, kline):\n        if symbol in self.last_moving_average_values and len(self.last_moving_average_values[symbol]) > 0:\n            self.eval_note = 0\n            last_price = kline[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value]\n            if last_price != self.last_candle_data[symbol][-1]:\n                await self._evaluate_current_price(last_price, cryptocurrency, symbol,\n                                                   evaluators_util.get_eval_time(kline=kline))\n\n    async def _evaluate_current_price(self, last_price, cryptocurrency, symbol, time):\n        last_ma_value = self.last_moving_average_values[symbol][-1]\n        if last_ma_value == 0:\n            self.eval_note = 0\n        else:\n            lower_threshold = last_ma_value * (1 - self.price_threshold)\n            upper_threshold = last_ma_value * (1 + self.price_threshold)\n            if lower_threshold < last_price < upper_threshold:\n                self.eval_note = 0\n            else:\n                current_ratio = last_price / last_ma_value\n                if current_ratio > 1:\n                    # last_price > last_ma_value => sell ? => eval_note > 0\n                    if current_ratio >= 2:\n                        self.eval_note = 1\n                    else:\n                        self.eval_note = current_ratio - 1\n                elif current_ratio < 1:\n                    # last_price < last_ma_value => buy ? => eval_note < 0\n                    self.eval_note = -1 * (1 - current_ratio)\n                else:\n                    self.eval_note = 0\n\n        await self.evaluation_completed(cryptocurrency, symbol, self.available_time_frame,\n                                        eval_time=time)\n\n    async def start(self, bot_id: str) -> bool:\n        \"\"\"\n        Subscribe to Kline and OHLCV notification\n        :return: bool\n        \"\"\"\n        try:\n            import octobot_trading.exchange_channel as exchange_channels\n            import octobot_trading.api as trading_api\n            exchange_id = trading_api.get_exchange_id_from_matrix_id(self.exchange_name, self.matrix_id)\n            await exchange_channels.get_chan(channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,\n                                             exchange_id).new_consumer(\n                callback=self.ohlcv_callback, time_frame=self.available_time_frame, priority_level=self.priority_level)\n            await exchange_channels.get_chan(channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,\n                                             exchange_id).new_consumer(\n                callback=self.kline_callback, time_frame=self.available_time_frame, priority_level=self.priority_level)\n            return True\n        except ImportError:\n            self.logger.error(\"Can't connect to trading channels\")\n        return False\n\n    def set_default_config(self):\n        super().set_default_config()\n        self.specific_config[commons_constants.CONFIG_TIME_FRAME] = \"1m\"\n\n    @staticmethod\n    def _compare_data(new_data, old_data):\n        try:\n            if new_data[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value][-1] != \\\n                    old_data[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value][-1]:\n                return False\n            return True\n        except Exception:\n            return False\n"
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"InstantFluctuationsEvaluator\", \"InstantMAEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/resources/InstantFluctuationsEvaluator.md",
    "content": "Triggers when a superior to 1% change of price or a superior to x4 change of volume from recent average happens.\n\nThe price distance from recent average is defining the strength the evaluation."
  },
  {
    "path": "Evaluator/RealTime/instant_fluctuations_evaluator/resources/InstantMAEvaluator.md",
    "content": "Uses a [moving average](https://www.investopedia.com/terms/m/movingaverage.asp) \ncomputed on close prices to set its evaluation. \n\nWill trigger an evaluation when the current close price is beyond the given price threshold applied on\nthe latest moving average value.\n\nTriggers on each new candle and price change. \n"
  },
  {
    "path": "Evaluator/Social/forum_evaluator/__init__.py",
    "content": "from .forum import RedditForumEvaluator"
  },
  {
    "path": "Evaluator/Social/forum_evaluator/config/RedditForumEvaluator.json",
    "content": "{\n    \"crypto-currencies\": [\n        {\n            \"crypto-currency\": \"Bitcoin\",\n            \"subreddits\": [\n                \"Bitcoin\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"Ethereum\",\n            \"subreddits\": [\n                \"ethereum\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"NEO\",\n            \"subreddits\": [\n                \"NEO\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"ICON\",\n            \"subreddits\": [\n                \"icon\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"NANO\",\n            \"subreddits\": [\n                \"nanocurrency\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"VeChain\",\n            \"subreddits\": [\n                \"Vechain\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"VeChain Thor\",\n            \"subreddits\": [\n                \"Vechain\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"Substratum\",\n            \"subreddits\": [\n                \"SubstratumNetwork\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"Ethos\",\n            \"subreddits\": [\n                \"ethos_io\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"Ontology\",\n            \"subreddits\": [\n                \"OntologyNetwork\"\n            ]\n        },\n        {\n            \"crypto-currency\": \"Binance Coin\",\n            \"subreddits\": []\n        }\n    ]\n}"
  },
  {
    "path": "Evaluator/Social/forum_evaluator/forum.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_services.constants as services_constants\nimport tentacles.Services.Services_feeds as Services_feeds\nimport tentacles.Evaluator.Util as EvaluatorUtil\n\nCONFIG_REDDIT = \"reddit\"\nCONFIG_REDDIT_SUBREDDITS = \"subreddits\"\nCONFIG_REDDIT_ENTRY = \"entry\"\nCONFIG_REDDIT_ENTRY_WEIGHT = \"entry_weight\"\n\n\n# RedditForumEvaluator is used to get an overall state of a market, it will not trigger a trade\n# (notify its evaluators) but is used to measure hype and trend of a market.\nclass RedditForumEvaluator(evaluators.SocialEvaluator):\n    SERVICE_FEED_CLASS = Services_feeds.RedditServiceFeed if hasattr(Services_feeds, 'RedditServiceFeed') else None\n\n    def __init__(self, tentacles_setup_config):\n        evaluators.SocialEvaluator.__init__(self, tentacles_setup_config)\n        self.overall_state_analyser = EvaluatorUtil.OverallStateAnalyser()\n        self.count = 0\n        self.sentiment_analyser = None\n        self.is_self_refreshing = True\n        self.subreddits_by_cryptocurrency = {}\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        cryptocurrencies = []\n        config_cryptocurrencies = self.UI.user_input(\n            commons_constants.CONFIG_CRYPTO_CURRENCIES, commons_enums.UserInputTypes.OBJECT_ARRAY,\n            cryptocurrencies, inputs, other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n            item_title=\"Crypto currency\",\n            title=\"Crypto currencies to watch.\"\n        )\n        # init one user input to generate user input schema and default values\n        cryptocurrencies.append(self._init_cryptocurrencies(inputs, \"Bitcoin\", [\"Bitcoin\"]))\n        # remove other symbols data to avoid unnecessary entries\n        self.subreddits_by_cryptocurrency = self._get_config_elements(config_cryptocurrencies, CONFIG_REDDIT_SUBREDDITS)\n        self.feed_config[services_constants.CONFIG_REDDIT_SUBREDDITS] = self.subreddits_by_cryptocurrency\n\n    def _init_cryptocurrencies(self, inputs, cryptocurrency, subreddits):\n        return {\n            commons_constants.CONFIG_CRYPTO_CURRENCY:\n                self.UI.user_input(commons_constants.CONFIG_CRYPTO_CURRENCY, commons_enums.UserInputTypes.TEXT,\n                                cryptocurrency, inputs, other_schema_values={\"minLength\": 2},\n                                parent_input_name=commons_constants.CONFIG_CRYPTO_CURRENCIES, array_indexes=[0],\n                                title=\"Crypto currency name\"),\n            CONFIG_REDDIT_SUBREDDITS:\n                self.UI.user_input(CONFIG_REDDIT_SUBREDDITS, commons_enums.UserInputTypes.STRING_ARRAY,\n                                subreddits, inputs, other_schema_values={\"uniqueItems\": True},\n                                parent_input_name=commons_constants.CONFIG_CRYPTO_CURRENCIES, array_indexes=[0],\n                                item_title=\"Subreddit name\",\n                                title=\"Subreddits to watch\")\n        }\n\n    @classmethod\n    def get_is_cryptocurrencies_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency dependant else False\n        \"\"\"\n        return False\n\n    @classmethod\n    def get_is_cryptocurrency_name_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency name dependant else False\n        \"\"\"\n        return False\n\n    def _print_entry(self, entry_text, entry_note, count=\"\"):\n        self.logger.debug(f\"New reddit entry ! : {entry_note} | {count} : {self.cryptocurrency_name} : \"\n                          f\"Link : {entry_text}\")\n\n    async def _feed_callback(self, data):\n        if self._is_interested_by_this_notification(data[services_constants.FEED_METADATA]):\n            self.count += 1\n            entry_note = self._get_sentiment(data[CONFIG_REDDIT_ENTRY])\n            if entry_note != commons_constants.START_PENDING_EVAL_NOTE:\n                self.overall_state_analyser.add_evaluation(entry_note, data[CONFIG_REDDIT_ENTRY_WEIGHT], False)\n                if data[CONFIG_REDDIT_ENTRY_WEIGHT] > 3:\n                    link = f\"https://www.reddit.com{data[CONFIG_REDDIT_ENTRY].permalink}\"\n                    self._print_entry(link, entry_note, str(self.count))\n                self.eval_note = self.overall_state_analyser.get_overall_state_after_refresh()\n                await self.evaluation_completed(self.cryptocurrency, eval_time=self.get_current_exchange_time())\n\n    def _get_sentiment(self, entry):\n        # analysis entry text and gives overall sentiment\n        reddit_entry_min_length = 50\n        # ignore usless (very short) entries\n        if entry.selftext and len(entry.selftext) >= reddit_entry_min_length:\n            return -1 * self.sentiment_analyser.analyse(entry.selftext)\n        return commons_constants.START_PENDING_EVAL_NOTE\n\n    def _is_interested_by_this_notification(self, notification_description):\n        # true if the given subreddit is in this cryptocurrency's subreddits configuration\n        try:\n            for subreddit in self.subreddits_by_cryptocurrency[self.cryptocurrency_name]:\n                if subreddit.lower() == notification_description:\n                    return True\n        except KeyError:\n            pass\n        return False\n\n    def _get_config_elements(self, config_cryptocurrencies, key):\n        if config_cryptocurrencies:\n            return {\n                cc[commons_constants.CONFIG_CRYPTO_CURRENCY]: cc[key]\n                for cc in config_cryptocurrencies\n                if cc[commons_constants.CONFIG_CRYPTO_CURRENCY] == self.cryptocurrency_name\n            }\n        return {}\n\n    async def prepare(self):\n        self.sentiment_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.TextAnalysis)()\n"
  },
  {
    "path": "Evaluator/Social/forum_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"RedditForumEvaluator\"],\n  \"tentacles-requirements\": [\"overall_state_analysis\", \"text_analysis\", \"reddit_service_feed\"]\n}"
  },
  {
    "path": "Evaluator/Social/forum_evaluator/resources/RedditForumEvaluator.md",
    "content": "First initialises using the recent history of the subreddits in RedditForumEvaluator.json then\nwatches for new posts to update its evaluation. \n\nNever triggers strategies re-evaluations, acts as a background evaluator\n"
  },
  {
    "path": "Evaluator/Social/news_evaluator/__init__.py",
    "content": "from .news import TwitterNewsEvaluator"
  },
  {
    "path": "Evaluator/Social/news_evaluator/config/TwitterNewsEvaluator.json",
    "content": "{\n    \"crypto-currencies\": [\n        {\n            \"crypto-currency\": \"Bitcoin\",\n            \"accounts\": [\n                \"BTCFoundation\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"Ethereum\",\n            \"accounts\": [\n                \"ethereum\",\n                \"VitalikButerin\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"Neo\",\n            \"accounts\": [\n                \"NEO_Blockchain\",\n                \"NEOnewstoday\",\n                \"NEO_council\",\n                \"neotogas\",\n                \"NEO_DevCon\",\n                \"neonexchange\",\n                \"dahongfei\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"ICON\",\n            \"accounts\": [\n                \"helloiconworld\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"NANO\",\n            \"accounts\": [\n                \"nanocurrency\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"VeChain\",\n            \"accounts\": [\n                \"sunshinelu24\",\n                \"VechainThorCom\",\n                \"Vechain1\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"VeChain Thor\",\n            \"accounts\": [\n                \"sunshinelu24\",\n                \"VechainThorCom\",\n                \"Vechain1\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"Substratum\",\n            \"accounts\": [\n                \"SubstratumNet\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"Ethos\",\n            \"accounts\": [\n                \"Ethos_io\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"Ontology\",\n            \"accounts\": [\n                \"OntologyNetwork\"\n            ],\n            \"hashtags\": []\n        },\n        {\n            \"crypto-currency\": \"Binance Coin\",\n            \"accounts\": [],\n            \"hashtags\": []\n        }\n    ]\n}"
  },
  {
    "path": "Evaluator/Social/news_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TwitterNewsEvaluator\"],\n  \"tentacles-requirements\": [\"text_analysis\", \"twitter_service_feed\"]\n}"
  },
  {
    "path": "Evaluator/Social/news_evaluator/news.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\n\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_services.constants as services_constants\nimport octobot_evaluators.evaluators as evaluators\nfrom tentacles.Evaluator.Util.text_analysis import TextAnalysis\nimport tentacles.Services.Services_feeds as Services_feeds\n\n\n# disable inheritance to disable tentacle visibility. Disabled as starting from feb 9 2023, API is now paid only\n# class TwitterNewsEvaluator(evaluators.SocialEvaluator):\nclass TwitterNewsEvaluator:\n    SERVICE_FEED_CLASS = Services_feeds.TwitterServiceFeed if hasattr(Services_feeds, 'TwitterServiceFeed') else None\n\n    # max time to live for a pulse is 10min\n    _EVAL_MAX_TIME_TO_LIVE = 10 * commons_constants.MINUTE_TO_SECONDS\n    # absolute value above which a notification is triggered\n    _EVAL_NOTIFICATION_THRESHOLD = 0.6\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.count = 0\n        self.sentiment_analyser = None\n        self.is_self_refreshing = True\n        self.accounts_by_cryptocurrency = {}\n        self.hashtags_by_cryptocurrency = {}\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        cryptocurrencies = []\n        config_cryptocurrencies = self.UI.user_input(\n            commons_constants.CONFIG_CRYPTO_CURRENCIES, commons_enums.UserInputTypes.OBJECT_ARRAY,\n            cryptocurrencies, inputs, other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n            item_title=\"Crypto currency\",\n            title=\"Crypto currencies to watch.\"\n        )\n        # init one user input to generate user input schema and default values\n        cryptocurrencies.append(self._init_cryptocurrencies(inputs, \"Bitcoin\", [\"BTCFoundation\"], []))\n        # remove other symbols data to avoid unnecessary entries\n        self.accounts_by_cryptocurrency = self._get_config_elements(config_cryptocurrencies,\n                                                                    services_constants.CONFIG_TWITTERS_ACCOUNTS)\n        self.hashtags_by_cryptocurrency = self._get_config_elements(config_cryptocurrencies,\n                                                                    services_constants.CONFIG_TWITTERS_HASHTAGS)\n        self.feed_config[services_constants.CONFIG_TWITTERS_ACCOUNTS] = self.accounts_by_cryptocurrency\n        self.feed_config[services_constants.CONFIG_TWITTERS_HASHTAGS] = self.hashtags_by_cryptocurrency\n\n    def _init_cryptocurrencies(self, inputs, cryptocurrency, accounts, hashtags):\n        return {\n            commons_constants.CONFIG_CRYPTO_CURRENCY:\n                self.UI.user_input(commons_constants.CONFIG_CRYPTO_CURRENCY, commons_enums.UserInputTypes.TEXT,\n                                cryptocurrency, inputs, other_schema_values={\"minLength\": 2},\n                                parent_input_name=commons_constants.CONFIG_CRYPTO_CURRENCIES, array_indexes=[0],\n                                title=\"Crypto currency name\"),\n            services_constants.CONFIG_TWITTERS_ACCOUNTS:\n                self.UI.user_input(services_constants.CONFIG_TWITTERS_ACCOUNTS, commons_enums.UserInputTypes.STRING_ARRAY,\n                                accounts, inputs, other_schema_values={\"uniqueItems\": True},\n                                parent_input_name=commons_constants.CONFIG_CRYPTO_CURRENCIES, array_indexes=[0],\n                                item_title=\"Twitter account name\",\n                                title=\"Twitter accounts to watch\"),\n            services_constants.CONFIG_TWITTERS_HASHTAGS:\n                self.UI.user_input(services_constants.CONFIG_TWITTERS_HASHTAGS, commons_enums.UserInputTypes.STRING_ARRAY,\n                                hashtags, inputs, other_schema_values={\"uniqueItems\": True},\n                                parent_input_name=commons_constants.CONFIG_CRYPTO_CURRENCIES, array_indexes=[0],\n                                item_title=\"Hashtag\",\n                                title=\"Twitter hashtags to watch (without the # character), \"\n                                      \"warning: might trigger evaluator for irrelevant tweets.\")\n        }\n\n    @classmethod\n    def get_is_cryptocurrencies_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency dependant else False\n        \"\"\"\n        return False\n\n    @classmethod\n    def get_is_cryptocurrency_name_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency name dependant else False\n        \"\"\"\n        return False\n\n    def _print_tweet(self, tweet_text, tweet_url, note, count=\"\"):\n        self.logger.debug(f\"Current note : {note} | {count} : {self.cryptocurrency_name} : Link: {tweet_url} Text : \"\n                          f\"{tweet_text.encode('utf-8', 'ignore')}\")\n\n    async def _feed_callback(self, data):\n        if self._is_interested_by_this_notification(data[services_constants.CONFIG_TWEET_DESCRIPTION]):\n            self.count += 1\n            note = self._get_tweet_sentiment(data[services_constants.CONFIG_TWEET],\n                                             data[services_constants.CONFIG_TWEET_DESCRIPTION])\n            tweet_url = f\"https://twitter.com/ProducToken/status/{data['tweet']['id']}\"\n            if note != commons_constants.START_PENDING_EVAL_NOTE:\n                self._print_tweet(data[services_constants.CONFIG_TWEET_DESCRIPTION], tweet_url, note, str(self.count))\n            await self._check_eval_note(note)\n\n    # only set eval note when something is happening\n    async def _check_eval_note(self, note):\n        if note != commons_constants.START_PENDING_EVAL_NOTE:\n            if abs(note) > self._EVAL_NOTIFICATION_THRESHOLD:\n                self.eval_note = note\n                self.save_evaluation_expiration_time(self._compute_notification_time_to_live(self.eval_note))\n                await self.evaluation_completed(self.cryptocurrency, eval_time=self.get_current_exchange_time())\n\n    @staticmethod\n    def _compute_notification_time_to_live(evaluation):\n        return TwitterNewsEvaluator._EVAL_MAX_TIME_TO_LIVE * abs(evaluation)\n\n    def _get_tweet_sentiment(self, tweet, tweet_text, is_a_quote=False):\n        try:\n            if is_a_quote:\n                return -1 * self.sentiment_analyser.analyse(tweet_text)\n            else:\n                padding_name = \"########\"\n                author_screen_name = tweet['user']['screen_name'] if \"screen_name\" in tweet['user'] \\\n                    else padding_name\n                author_name = tweet['user']['name'] if \"name\" in tweet['user'] else padding_name\n                if author_screen_name in self.accounts_by_cryptocurrency[self.cryptocurrency_name] \\\n                        or author_name in self.accounts_by_cryptocurrency[self.cryptocurrency_name]:\n                    return -1 * self.sentiment_analyser.analyse(tweet_text)\n        except KeyError:\n            pass\n\n        # ignore # for the moment (too much of bullshit)\n        return commons_constants.START_PENDING_EVAL_NOTE\n\n    def _is_interested_by_this_notification(self, notification_description):\n        # true if in twitter accounts\n        try:\n            for account in self.accounts_by_cryptocurrency[self.cryptocurrency_name]:\n                if account.lower() in notification_description:\n                    return True\n        except KeyError:\n            return False\n        # false if it's a RT of an unfollowed account\n        if notification_description.startswith(\"rt\"):\n            return False\n\n        # true if contains symbol\n        if self.cryptocurrency_name.lower() in notification_description:\n            return True\n\n        # true if in hashtags\n        if self.hashtags_by_cryptocurrency:\n            for hashtags in self.hashtags_by_cryptocurrency[self.cryptocurrency_name]:\n                if hashtags.lower() in notification_description:\n                    return True\n            return False\n\n    def _get_config_elements(self, config_cryptocurrencies, key):\n        if config_cryptocurrencies:\n            return {\n                cc[commons_constants.CONFIG_CRYPTO_CURRENCY]: cc[key]\n                for cc in config_cryptocurrencies\n                if cc[commons_constants.CONFIG_CRYPTO_CURRENCY] == self.cryptocurrency_name\n            }\n        return {}\n\n    async def prepare(self):\n        self.sentiment_analyser = tentacles_management.get_single_deepest_child_class(TextAnalysis)()\n"
  },
  {
    "path": "Evaluator/Social/news_evaluator/resources/TwitterNewsEvaluator.md",
    "content": "Triggers when a new tweet appears from a Twitter account in TwitterNewsEvaluator.json.\n\nIf the evaluation of any given tweet is significant enough, triggers strategies re-evaluation. Otherwise \nacts as a background evaluator."
  },
  {
    "path": "Evaluator/Social/signal_evaluator/__init__.py",
    "content": "from .signal import TelegramSignalEvaluator, TelegramChannelSignalEvaluator"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/config/TelegramChannelSignalEvaluator.json",
    "content": "{\n    \"telegram-channels\": [\n        {\n            \"channel_name\": \"Test-Channel\",\n            \"signal_pair\": \"Pair: (.*)$\",\n            \"signal_pattern\": {\n                \"MARKET_BUY\": \"Side: (BUY)$\",\n                \"MARKET_SELL\": \"Side: (SELL)$\"\n            }\n        }\n    ]\n}"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/config/TelegramSignalEvaluator.json",
    "content": "{\n    \"telegram-channels\": [\n        \"test_telegram_signal_strat\"\n    ]\n}"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramSignalEvaluator\", \"TelegramChannelSignalEvaluator\"],\n  \"tentacles-requirements\": [\"telegram_service_feed\"]\n}"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/resources/TelegramChannelSignalEvaluator.md",
    "content": "Evaluator that catch Telegram channel signals.\n\nTriggers on a Telegram signal from any channel your personal account joined.\n\nSignal parsing is configurable according to the name of the channel.\n\nSee [OctoBot docs about Telegram API service](https://www.octobot.cloud/en/guides/octobot-interfaces/telegram/telegram-api?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=telegramChannelSignalEvaluator) for more information.\n"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/resources/TelegramSignalEvaluator.md",
    "content": "Very simple evaluator designed to be an example for an evaluator using Telegram signals.\n\nTriggers on a Telegram signal from any group or channel listed in this evaluator configuration in which \nyour Telegram bot is invited.\n\nSignal format for this implementation is: **SYMBOL[evaluation]**. Example: **BTC/USDT[-0.45]**.\n\nSYMBOL has to be in current watched symbols (in configuration) and evaluation must be between -1 and 1. \n\nRemember that OctoBot can only see messages from a\nchat/group where its Telegram bot (in OctoBot configuration) has been invited. Keep also in mind that you\nneed to disable the privacy mode of your Telegram bot to allow it to see group messages.\n\nSee [OctoBot docs about Telegram interface](https://www.octobot.cloud/en/guides/octobot-interfaces/telegram?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=telegramSignalEvaluator) for more information.\n"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/signal.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport re\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_services.constants as services_constants\nimport octobot_evaluators.evaluators as evaluators\nimport tentacles.Services.Services_feeds as Services_feeds\n\n\nclass TelegramSignalEvaluator(evaluators.SocialEvaluator):\n    SERVICE_FEED_CLASS = Services_feeds.TelegramServiceFeed if hasattr(Services_feeds, 'TelegramServiceFeed') else None\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        channels_config = self.UI.user_input(services_constants.CONFIG_TELEGRAM_CHANNEL,\n                                          commons_enums.UserInputTypes.STRING_ARRAY,\n                                          [], inputs, item_title=\"Channel name\",\n                                          title=\"Name of the watched channels\")\n        self.feed_config[services_constants.CONFIG_TELEGRAM_CHANNEL] = channels_config\n\n    async def _feed_callback(self, data):\n        if self._is_interested_by_this_notification(data[services_constants.CONFIG_GROUP_MESSAGE_DESCRIPTION]):\n            await self.analyse_notification(data)\n            await self.evaluation_completed(self.cryptocurrency, self.symbol,\n                                            eval_time=self.get_current_exchange_time())\n        else:\n            self.logger.debug(f\"Ignored telegram feed: \\\"{self.symbol.lower()}\\\" pattern not found in \"\n                              f\"\\\"{data[services_constants.CONFIG_GROUP_MESSAGE_DESCRIPTION].lower()}\\\"\")\n\n    # return true if the given notification is relevant for this client\n    def _is_interested_by_this_notification(self, notification_description):\n        if self.symbol:\n            return self.symbol.lower() in notification_description.lower()\n        else:\n            return True\n\n    async def analyse_notification(self, notification):\n        notification_test = notification[services_constants.CONFIG_GROUP_MESSAGE_DESCRIPTION]\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        start_eval_chars = \"[\"\n        end_eval_chars = \"]\"\n        if start_eval_chars in notification_test and end_eval_chars in notification_test:\n            try:\n                split_test = notification_test.split(start_eval_chars)\n                notification_eval = split_test[1].split(end_eval_chars)[0]\n                potential_note = float(notification_eval)\n                if -1 <= potential_note <= 1:\n                    self.eval_note = potential_note\n                else:\n                    self.logger.error(f\"Impossible to use notification evaluation: {notification_eval}: \"\n                                      f\"evaluation should be between -1 and 1.\")\n            except Exception as e:\n                self.logger.error(f\"Impossible to parse notification {notification_test}: {e}. Please refer to this \"\n                                  f\"evaluator documentation to check the notification pattern.\")\n        else:\n            self.logger.error(f\"Impossible to parse notification {notification_test}. Please refer to this evaluator \"\n                              f\"documentation to check the notification pattern.\")\n\n    @classmethod\n    def get_is_cryptocurrencies_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency dependant else False\n        \"\"\"\n        return False\n\n    @classmethod\n    def get_is_cryptocurrency_name_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency name dependant else False\n        \"\"\"\n        return False\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not symbol dependant else False\n        \"\"\"\n        return False\n\n    def _get_tentacle_registration_topic(self, all_symbols_by_crypto_currencies, time_frames, real_time_time_frames):\n        currencies = [self.cryptocurrency]\n        symbols = [self.symbol]\n        to_handle_time_frames = [self.time_frame]\n        if self.get_is_cryptocurrencies_wildcard():\n            currencies = all_symbols_by_crypto_currencies.keys()\n        if self.get_is_symbol_wildcard():\n            symbols = []\n            for currency_symbols in all_symbols_by_crypto_currencies.values():\n                symbols += currency_symbols\n        # by default no time frame registration for social evaluators\n        return currencies, symbols, to_handle_time_frames\n\n\nclass TelegramChannelSignalEvaluator(evaluators.SocialEvaluator):\n    SERVICE_FEED_CLASS = Services_feeds.TelegramApiServiceFeed if hasattr(Services_feeds, 'TelegramApiServiceFeed') else None\n\n    SIGNAL_PATTERN_KEY = \"signal_pattern\"\n    SIGNAL_PATTERN_MARKET_BUY_KEY = \"MARKET_BUY\"\n    SIGNAL_PATTERN_MARKET_SELL_KEY = \"MARKET_SELL\"\n    SIGNAL_PAIR_KEY = \"signal_pair\"\n    SIGNAL_CHANNEL_NAME_KEY = \"channel_name\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.channels_config_by_channel_name = {}\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        channels = []\n        config_channels = self.UI.user_input(services_constants.CONFIG_TELEGRAM_CHANNEL,\n                                          commons_enums.UserInputTypes.OBJECT_ARRAY,\n                                          channels, inputs, item_title=\"Channel\",\n                                          other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n                                          title=\"Channels to watch\")\n        channels.append(self._init_channel_config(inputs, \"Test-Channel\", \"Pair: (.*)$\",\n                                                  \"Side: (BUY)$\", \"Side: (SELL)$\"))\n        self.channels_config_by_channel_name = {\n            channel[self.SIGNAL_CHANNEL_NAME_KEY]: channel\n            for channel in config_channels\n        }\n        self.feed_config[services_constants.CONFIG_TELEGRAM_CHANNEL] = list(self.channels_config_by_channel_name)\n\n    def _init_channel_config(self, inputs, channel_name, signal_pair, buy_regex, sell_regex):\n        return {\n            self.SIGNAL_CHANNEL_NAME_KEY: self.UI.user_input(\n                self.SIGNAL_CHANNEL_NAME_KEY, commons_enums.UserInputTypes.TEXT,\n                channel_name, inputs,\n                parent_input_name=services_constants.CONFIG_TELEGRAM_CHANNEL,\n                array_indexes=[0],\n                title=\"Channel name\"),\n            self.SIGNAL_PAIR_KEY: self.UI.user_input(\n                self.SIGNAL_PAIR_KEY, commons_enums.UserInputTypes.TEXT,\n                signal_pair, inputs,\n                parent_input_name=services_constants.CONFIG_TELEGRAM_CHANNEL,\n                array_indexes=[0],\n                title=\"Trading pair regex, ex: Pair: (.*)$\"),\n            self.SIGNAL_PATTERN_KEY: self.UI.user_input(\n                self.SIGNAL_PATTERN_KEY, commons_enums.UserInputTypes.OBJECT,\n                self._init_pattern_config(inputs, buy_regex, sell_regex), inputs,\n                parent_input_name=services_constants.CONFIG_TELEGRAM_CHANNEL,\n                array_indexes=[0],\n                title=\"Signal patterns\"),\n        }\n\n    def _init_pattern_config(self, inputs, buy_regex, sell_regex):\n        return {\n            self.SIGNAL_PATTERN_MARKET_BUY_KEY: self.UI.user_input(\n                self.SIGNAL_PATTERN_MARKET_BUY_KEY, commons_enums.UserInputTypes.TEXT,\n                buy_regex, inputs, parent_input_name=self.SIGNAL_PATTERN_KEY,\n                array_indexes=[0],\n                title=\"Market buy signal regex, ex: Side: (BUY)$\"),\n            self.SIGNAL_PATTERN_MARKET_SELL_KEY: self.UI.user_input(\n                self.SIGNAL_PATTERN_MARKET_SELL_KEY,\n                commons_enums.UserInputTypes.TEXT,\n                sell_regex, inputs,\n                parent_input_name=self.SIGNAL_PATTERN_KEY,\n                array_indexes=[0],\n                title=\"Market sell signal regex, ex: Side: (SELL)$\"),\n        }\n\n    async def _feed_callback(self, data):\n        if not data:\n            return\n        is_from_channel = data.get(services_constants.CONFIG_IS_CHANNEL_MESSAGE, False)\n        if is_from_channel:\n            sender = data.get(services_constants.CONFIG_MESSAGE_SENDER, \"\")\n            if sender in self.channels_config_by_channel_name:\n                try:\n                    message = data.get(services_constants.CONFIG_MESSAGE_CONTENT, \"\")\n                    channel_data = self.channels_config_by_channel_name[sender]\n                    is_buy_market_signal = self._get_signal_message(\n                        channel_data[self.SIGNAL_PATTERN_KEY][self.SIGNAL_PATTERN_MARKET_BUY_KEY], message)\n                    is_sell_market_signal = self._get_signal_message(\n                        channel_data[self.SIGNAL_PATTERN_KEY][self.SIGNAL_PATTERN_MARKET_SELL_KEY], message)\n                    pair = self._get_signal_message(channel_data[self.SIGNAL_PAIR_KEY], message)\n                    if (is_buy_market_signal or is_sell_market_signal) and pair is not None:\n                        self.eval_note = -1 if is_buy_market_signal else 1\n                        await self.evaluation_completed(symbol=pair.strip(), eval_time=self.get_current_exchange_time())\n                    else:\n                        self.logger.warning(f\"Unable to parse message from {sender} : {message}\")\n                except KeyError:\n                    self.logger.warning(f\"Unable to parse message from {sender}\")\n            else:\n                self.logger.debug(f\"Ignored message : from an unsupported channel ({sender})\")\n        else:\n            self.logger.debug(\"Ignored message : not a channel message\")\n\n    def _get_signal_message(self, expected_pattern, message):\n        try:\n            match = re.search(expected_pattern, message)\n            return match.group(1)\n        except AttributeError:\n            self.logger.debug(f\"Ignored message : not matching channel pattern ({message})\")\n        return None\n"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Evaluator/Social/signal_evaluator/tests/test_telegram_channel_signal_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.logging as logging\nimport octobot_services.constants as services_constants\nimport tentacles.Evaluator.Social as Social\nimport tests.test_utils.config as test_utils_config\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def _trigger_callback_with_data_and_assert_note(evaluator: Social.TelegramChannelSignalEvaluator,\n                                                      data=None,\n                                                      note=commons_constants.START_PENDING_EVAL_NOTE):\n    await evaluator._feed_callback(data)\n    assert evaluator.eval_note == note\n    evaluator.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n\n\ndef _create_evaluator_with_supported_channel_signals():\n    evaluator = Social.TelegramChannelSignalEvaluator(test_utils_config.load_test_tentacles_config())\n    evaluator.logger = logging.get_logger(evaluator.get_name())\n    evaluator.specific_config = {\n        \"telegram-channels\": [\n            {\n                \"channel_name\": \"TEST-CHAN-1\",\n                \"signal_pattern\": {\n                    \"MARKET_BUY\": \"Side: (BUY)\",\n                    \"MARKET_SELL\": \"Side: (SELL)\"\n                },\n                \"signal_pair\": \"Pair: (.*)\"\n            },\n            {\n                \"channel_name\": \"TEST-CHAN-2\",\n                \"signal_pattern\": {\n                    \"MARKET_BUY\": \".* : (-1)$\",\n                    \"MARKET_SELL\": \".* : (1)$\"\n                },\n                \"signal_pair\": \"(.*):\"\n            }\n        ]\n    }\n    evaluator.init_user_inputs({})\n    evaluator.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n    return evaluator\n\n\nasync def test_without_data():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator)\n\n\nasync def test_with_empty_data():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={})\n\n\nasync def test_incorrect_signal_without_sender_without_channel_message():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: False,\n        services_constants.CONFIG_MESSAGE_SENDER: \"\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\",\n    })\n\n\nasync def test_incorrect_signal_without_sender_with_channel_message():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\",\n    })\n\n\nasync def test_incorrect_signal_chan1_without_content():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-1\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\",\n    })\n\n\nasync def test_incorrect_signal_chan1_without_coin():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-1\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\"\"\n        Order Id: 1631033831358699\n        Pair: \n        Side:\n        Price: 12.909\n        \"\"\",\n    })\n\n\nasync def test_incorrect_signal_chan1_without_separator():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-1\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\"\"\n        Order Id: 1631033831358699\n        Pair QTUMUSDT\n        Side: BUY\n        Price: 12.909\n        \"\"\",\n    })\n\n\nasync def test_correct_signal_chan1_with_not_channel_message():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: False,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-1\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\"\"\n        Order Id: 1631033831358699\n        Pair: QTUMUSDT\n        Side: BUY\n        Price: 12.909\n        \"\"\",\n    })\n\n\nasync def test_correct_signal_chan1_with_chan2():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-2\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\"\"\n        Order Id: 1631033831358699\n        Pair: QTUMUSDT\n        Side: BUY\n        Price: 12.909\n        \"\"\",\n    })\n\n\nasync def test_correct_signal_chan1():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-1\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"\"\"\n        Order Id: 1631033831358699\n        Pair: QTUMUSDT\n        Side: BUY\n        Price: 12.909\n        \"\"\",\n    }, note=-1)\n\n\nasync def test_correct_signal_chan2_but_with_chan1():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-1\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"BTC/USDT : 1\",\n    })\n\n\nasync def test_correct_signal_chan2():\n    evaluator = _create_evaluator_with_supported_channel_signals()\n    await _trigger_callback_with_data_and_assert_note(evaluator, data={\n        services_constants.CONFIG_IS_CHANNEL_MESSAGE: True,\n        services_constants.CONFIG_MESSAGE_SENDER: \"TEST-CHAN-2\",\n        services_constants.CONFIG_MESSAGE_CONTENT: \"BTC/USDT : -1\",\n    }, note=-1)\n"
  },
  {
    "path": "Evaluator/Social/trends_evaluator/__init__.py",
    "content": "from .trends import GoogleTrendsEvaluator"
  },
  {
    "path": "Evaluator/Social/trends_evaluator/config/GoogleTrendsEvaluator.json",
    "content": "{\n  \"refresh_rate_seconds\" : 86400,\n  \"relevant_history_months\" : 3\n}\n"
  },
  {
    "path": "Evaluator/Social/trends_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GoogleTrendsEvaluator\"],\n  \"tentacles-requirements\": [\"statistics_analysis\", \"google_service_feed\"]\n}"
  },
  {
    "path": "Evaluator/Social/trends_evaluator/resources/GoogleTrendsEvaluator.md",
    "content": "Analyses the popularity of the given currencies using their names. \n\nData are provided by [Google's trends service](https://trends.google.com/trends/?geo=US).\n \nDue to Google trends poor refresh rate, this evaluation should be considered for large time frames only."
  },
  {
    "path": "Evaluator/Social/trends_evaluator/trends.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport numpy\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_services.constants as services_constants\nimport tentacles.Evaluator.Util as EvaluatorUtil\nimport tentacles.Services.Services_feeds as Services_feeds\n\n\nclass GoogleTrendsEvaluator(evaluators.SocialEvaluator):\n    SERVICE_FEED_CLASS = Services_feeds.GoogleServiceFeed if hasattr(Services_feeds, 'GoogleServiceFeed') else None\n\n    def __init__(self, tentacles_setup_config):\n        evaluators.SocialEvaluator.__init__(self, tentacles_setup_config)\n        self.stats_analyser = None\n        self.refresh_rate_seconds = 86400\n        self.relevant_history_months = 3\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.refresh_rate_seconds = self.refresh_rate_seconds or \\\n            self.UI.user_input(commons_constants.CONFIG_REFRESH_RATE,\n                               commons_enums.UserInputTypes.INT,\n                               self.refresh_rate_seconds, inputs, min_val=1,\n                               title=\"Seconds between each re-evaluation (do not set too low because google has a low \"\n                                     \"monthly rate limit).\")\n        self.relevant_history_months = self.UI.user_input(services_constants.CONFIG_TREND_HISTORY_TIME,\n                                                       commons_enums.UserInputTypes.INT,\n                                                       self.relevant_history_months, inputs, min_val=3, max_val=3,\n                                                       title=\"Number of months to look into to compute the trend \"\n                                                             \"evaluation (for now works only with 3).\")\n        self.feed_config[services_constants.CONFIG_TREND_TOPICS] = self._build_trend_topics()\n\n    @classmethod\n    def get_is_cryptocurrencies_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency dependant else False\n        \"\"\"\n        return False\n\n    @classmethod\n    def get_is_cryptocurrency_name_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not cryptocurrency name dependant else False\n        \"\"\"\n        return False\n\n    async def _feed_callback(self, data):\n        if self._is_interested_by_this_notification(data[services_constants.FEED_METADATA]):\n            trend = numpy.array([d[\"data\"] for d in data[services_constants.CONFIG_TREND]])\n            # compute bollinger bands\n            self.eval_note = self.stats_analyser.analyse_recent_trend_changes(trend, numpy.sqrt)\n            await self.evaluation_completed(self.cryptocurrency, eval_time=self.get_current_exchange_time())\n\n    def _is_interested_by_this_notification(self, notification_description):\n        return self.cryptocurrency_name in notification_description\n\n    def _build_trend_topics(self):\n        trend_time_frame = f\"today {self.relevant_history_months}-m\"\n        return [\n            Services_feeds.TrendTopic(self.refresh_rate_seconds,\n                                      [self.cryptocurrency_name],\n                                      time_frame=trend_time_frame)\n        ]\n\n    async def prepare(self):\n        self.stats_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.StatisticAnalysis)()\n"
  },
  {
    "path": "Evaluator/Strategies/blank_strategy_evaluator/__init__.py",
    "content": "from .blank_strategy import BlankStrategyEvaluator\n"
  },
  {
    "path": "Evaluator/Strategies/blank_strategy_evaluator/blank_strategy.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.constants as common_constants\nimport octobot_commons.enums as common_enums\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.enums as enums\n\n\nclass BlankStrategyEvaluator(evaluators.StrategyEvaluator):\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        super().init_user_inputs(inputs)\n        self.UI.user_input(common_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT, common_enums.UserInputTypes.INT,\n                        200, inputs, min_val=1,\n                        title=\"Initialization candles count: the number of historical candles to fetch from \"\n                              \"exchanges when OctoBot is starting.\")\n\n    def get_full_cycle_evaluator_types(self) -> tuple:\n        # returns a tuple as it is faster to create than a list\n        return enums.EvaluatorMatrixTypes.TA.value, enums.EvaluatorMatrixTypes.SCRIPTED.value\n\n    async def matrix_callback(self,\n                              matrix_id,\n                              evaluator_name,\n                              evaluator_type,\n                              eval_note,\n                              eval_note_type,\n                              exchange_name,\n                              cryptocurrency,\n                              symbol,\n                              time_frame):\n        self.eval_note = eval_note\n        await self.strategy_completed(cryptocurrency, symbol, time_frame=time_frame)\n"
  },
  {
    "path": "Evaluator/Strategies/blank_strategy_evaluator/config/BlankStrategyEvaluator.json",
    "content": "{\n  \"required_time_frames\" : [\"1h\"],\n  \"required_evaluators\" : [\"*\"],\n  \"required_candles_count\" : 200,\n  \"default_config\" : [\"ScriptedEvaluator\"]\n}"
  },
  {
    "path": "Evaluator/Strategies/blank_strategy_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BlankStrategyEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Strategies/blank_strategy_evaluator/resources/BlankStrategyEvaluator.md",
    "content": "BlankStrategyEvaluator is forwarding evaluator values to the trading mode.\n"
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/__init__.py",
    "content": "from .dip_analyser_strategy import DipAnalyserStrategyEvaluator"
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/config/DipAnalyserStrategyEvaluator.json",
    "content": "{\n    \"default_config\": [\n        \"KlingerOscillatorReversalConfirmationMomentumEvaluator\",\n        \"RSIWeightMomentumEvaluator\"\n    ],\n    \"required_evaluators\": [\n        \"InstantFluctuationsEvaluator\",\n        \"KlingerOscillatorReversalConfirmationMomentumEvaluator\",\n        \"RSIWeightMomentumEvaluator\"\n    ],\n    \"required_time_frames\": [\n        \"4h\"\n    ]\n}"
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/dip_analyser_strategy.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_evaluators.api.matrix as evaluators_api\nimport octobot_evaluators.evaluators.channel as evaluator_channel\nimport octobot_evaluators.constants as evaluator_constants\nimport octobot_evaluators.matrix as matrix\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_trading.api as trading_api\nimport tentacles.Evaluator.TA as TA\n\n\nclass DipAnalyserStrategyEvaluator(evaluators.StrategyEvaluator):\n    REVERSAL_CONFIRMATION_CLASS_NAME = TA.KlingerOscillatorReversalConfirmationMomentumEvaluator.get_name()\n    REVERSAL_WEIGHT_CLASS_NAME = TA.RSIWeightMomentumEvaluator.get_name()\n\n    @staticmethod\n    def get_eval_type():\n        return typing.Dict[str, int]\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.evaluation_time_frame = None\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.evaluation_time_frame = self.evaluation_time_frame or commons_enums.TimeFrames(\n            self.UI.user_input(\n                evaluator_constants.STRATEGIES_REQUIRED_TIME_FRAME,\n                commons_enums.UserInputTypes.MULTIPLE_OPTIONS,\n                [commons_enums.TimeFrames.ONE_HOUR.value],\n                inputs, options=[tf.value for tf in commons_enums.TimeFrames],\n                title=\"Analysed time frame: only the first one will be considered for DipAnalyserStrategyEvaluator.\"\n            )[0]\n        ).value\n\n    async def matrix_callback(self,\n                              matrix_id,\n                              evaluator_name,\n                              evaluator_type,\n                              eval_note,\n                              eval_note_type,\n                              exchange_name,\n                              cryptocurrency,\n                              symbol,\n                              time_frame):\n        if evaluator_type == evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value:\n            # trigger re-evaluation\n            exchange_id = trading_api.get_exchange_id_from_matrix_id(exchange_name, matrix_id)\n            await evaluator_channel.trigger_technical_evaluators_re_evaluation_with_updated_data(matrix_id,\n                                                                                                 evaluator_name,\n                                                                                                 evaluator_type,\n                                                                                                 exchange_name,\n                                                                                                 cryptocurrency,\n                                                                                                 symbol,\n                                                                                                 exchange_id,\n                                                                                                 self.strategy_time_frames)\n            # do not continue this evaluation\n            return\n        elif evaluator_type == evaluators_enums.EvaluatorMatrixTypes.TA.value:\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n            TA_evaluations = matrix.get_evaluations_by_evaluator(matrix_id,\n                                                                 exchange_name,\n                                                                 evaluators_enums.EvaluatorMatrixTypes.TA.value,\n                                                                 cryptocurrency,\n                                                                 symbol,\n                                                                 self.evaluation_time_frame,\n                                                                 allowed_values=[\n                                                                     commons_constants.START_PENDING_EVAL_NOTE])\n\n            try:\n                if evaluators_api.get_value(TA_evaluations[self.REVERSAL_CONFIRMATION_CLASS_NAME]):\n                    self.eval_note = evaluators_api.get_value(TA_evaluations[self.REVERSAL_WEIGHT_CLASS_NAME])\n                await self.strategy_completed(cryptocurrency, symbol)\n            except KeyError as e:\n                self.logger.error(f\"Missing required evaluator: {e}\")\n"
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"DipAnalyserStrategyEvaluator\"],\n  \"tentacles-requirements\": [\"momentum_evaluator.py\"]\n}"
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/resources/DipAnalyserStrategyEvaluator.md",
    "content": "DipAnalyserStrategyEvaluator is a strategy analysing market dips using [RSI](https://www.investopedia.com/terms/r/rsi.asp) \naverages. According to the level of the RSI, a buy signal can be generated. This signal has a weight that corresponds to \na higher or lower intensity of the RSI evaluation.\n \nThis strategy also uses the [Klinger oscillator](https://www.investopedia.com/terms/k/klingeroscillator.asp) to identify \nreversals and create buy signals. \n\nA buy signal is generated when the RSI component is signaling an opportunity and the Klinger part is confirming \na reversal situation.\n\nThis strategy is updated at the end of each candle on the watched time frame. \n\nIt is also possible to make it trigger \nautomatically using a real-time evaluator. Using a real time evaluator that signals sudden market changes like the \nInstantFluctuationsEvaluator will make DipAnalyserStrategyEvaluator also wake up on such events.\n\nDipAnalyserStrategyEvaluator focuses on one time frame only and works best on larger time frames such as 4h and more."
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Evaluator/Strategies/dip_analyser_strategy_evaluator/tests/test_dip_analyser_strategy_evaluator.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport decimal\n\nimport tests.functional_tests.strategy_evaluators_tests.abstract_strategy_test as abstract_strategy_test\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Trading.Mode as Mode\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest.fixture\ndef strategy_tester():\n    strategy_tester_instance = DipAnalyserStrategiesEvaluatorTest()\n    strategy_tester_instance.initialize(Strategies.DipAnalyserStrategyEvaluator, Mode.DipAnalyserTradingMode)\n    return strategy_tester_instance\n\n\nclass DipAnalyserStrategiesEvaluatorTest(abstract_strategy_test.AbstractStrategyTest):\n    \"\"\"\n    About using this test framework:\n    To be called by pytest, tests have to be called manually since the cythonized version of AbstractStrategyTest\n    creates an __init__() which prevents the default pytest tests collect process\n    \"\"\"\n\n    # Careful with results here, unlike other strategy tests, this one uses only the 4h timeframe, therefore results\n    # are not comparable with regular 1h timeframes strategy tests\n\n    # Cannot use bittrex data since they are not providing 4h timeframe data\n\n    # test_full_mixed_strategies_evaluator.py with only 4h timeframe results are provided for comparison:\n    # format: results: (bot profitability, market average profitability)\n\n    async def test_default_run(self):\n        # market: -49.25407390406244\n        await self.run_test_default_run(decimal.Decimal(str(-24.612)))\n\n    async def test_slow_downtrend(self):\n        # market: -49.25407390406244\n        # market: -47.50593824228029\n        await self.run_test_slow_downtrend(decimal.Decimal(str(-24.612)), decimal.Decimal(str(-33.601)), None, None, skip_extended=True)\n\n    async def test_sharp_downtrend(self):\n        # market: -34.67997135795625\n        await self.run_test_sharp_downtrend(decimal.Decimal(str(-21.634)), None, skip_extended=True)\n\n    async def test_flat_markets(self):\n        # market: -38.07647740440325\n        # market: -53.87077652637819\n        await self.run_test_flat_markets(decimal.Decimal(str(-20.577)), decimal.Decimal(str(-32.756)), None, None, skip_extended=True)\n\n    async def test_slow_uptrend(self):\n        # market: 11.32644122514472\n        # market: -36.64596273291926\n        await self.run_test_slow_uptrend(decimal.Decimal(str(11.326)), decimal.Decimal(str(-14.248)))\n\n    async def test_sharp_uptrend(self):\n        # market: -17.047906776003458\n        # market: -18.25837965302341\n        await self.run_test_sharp_uptrend(decimal.Decimal(str(3.607)), decimal.Decimal(str(10.956)))\n\n    async def test_up_then_down(self):\n        await self.run_test_up_then_down(None, skip_extended=True)\n\n\nasync def test_default_run(strategy_tester):\n    await strategy_tester.test_default_run()\n\n\nasync def test_slow_downtrend(strategy_tester):\n    await strategy_tester.test_slow_downtrend()\n\n\nasync def test_sharp_downtrend(strategy_tester):\n    await strategy_tester.test_sharp_downtrend()\n\n\nasync def test_flat_markets(strategy_tester):\n    await strategy_tester.test_flat_markets()\n\n\nasync def test_slow_uptrend(strategy_tester):\n    await strategy_tester.test_slow_uptrend()\n\n\nasync def test_sharp_uptrend(strategy_tester):\n    await strategy_tester.test_sharp_uptrend()\n\n\nasync def test_up_then_down(strategy_tester):\n    await strategy_tester.test_up_then_down()\n"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/__init__.py",
    "content": "from .mixed_strategies import SimpleStrategyEvaluator, TechnicalAnalysisStrategyEvaluator"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/config/SimpleStrategyEvaluator.json",
    "content": "{\n    \"default_config\": [\n        \"DoubleMovingAverageTrendEvaluator\",\n        \"RSIMomentumEvaluator\"\n    ],\n    \"required_evaluators\": [\n        \"*\"\n    ],\n    \"required_time_frames\": [\n        \"1h\",\n        \"4h\",\n        \"1d\"\n    ],\n    \"required_candles_count\": 1000,\n    \"social_evaluators_notification_timeout\": 3600,\n    \"re_evaluate_TA_when_social_or_realtime_notification\": true,\n    \"background_social_evaluators\": [\n      \"RedditForumEvaluator\"\n    ]\n}"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/config/TechnicalAnalysisStrategyEvaluator.json",
    "content": "{\n    \"compatible_evaluator_types\": [\n        \"TA\",\n        \"REAL_TIME\"\n    ],\n    \"default_config\": [\n        \"DoubleMovingAverageTrendEvaluator\",\n        \"RSIMomentumEvaluator\"\n    ],\n    \"required_evaluators\": [\n        \"*\"\n    ],\n    \"required_time_frames\": [\n        \"30m\", \"1h\", \"2h\", \"4h\", \"1d\"\n    ],\n    \"time_frames_to_weight\": [\n        {\n            \"time_frame\": \"30m\",\n            \"weight\": 30\n        },\n        {\n            \"time_frame\": \"1h\",\n            \"weight\": 50\n        },\n        {\n            \"time_frame\": \"2h\",\n            \"weight\": 50\n        },\n        {\n            \"time_frame\": \"4h\",\n            \"weight\": 50\n        },\n        {\n            \"time_frame\": \"1d\",\n            \"weight\": 30\n        }\n    ]\n}"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"SimpleStrategyEvaluator\", \"TechnicalAnalysisStrategyEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/mixed_strategies.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.evaluators_util as evaluators_util\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_evaluators.api as evaluators_api\nimport octobot_evaluators.evaluators.channel as evaluators_channel\nimport octobot_evaluators.matrix as matrix\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_evaluators.constants as evaluators_constants\nimport octobot_evaluators.errors as errors\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_tentacles_manager.api.configurator as tentacles_manager_api\nimport octobot_tentacles_manager.configuration as tm_configuration\nimport octobot_trading.api as trading_api\n\n\nclass SimpleStrategyEvaluator(evaluators.StrategyEvaluator):\n    SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY = \"social_evaluators_notification_timeout\"\n    RE_EVAL_TA_ON_RT_OR_SOCIAL = \"re_evaluate_TA_when_social_or_realtime_notification\"\n    BACKGROUND_SOCIAL_EVALUATORS = \"background_social_evaluators\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.re_evaluation_triggering_eval_types = [evaluators_enums.EvaluatorMatrixTypes.SOCIAL.value,\n                                                    evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value]\n        self.social_evaluators_default_timeout = None\n        self.re_evaluate_TA_when_social_or_realtime_notification = True\n        self.background_social_evaluators = []\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        super().init_user_inputs(inputs)\n        default_config = self.get_default_config()\n        self.UI.user_input(commons_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT, commons_enums.UserInputTypes.INT,\n                        default_config[commons_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT],\n                       inputs, min_val=1,\n                        title=\"Initialization candles count: the number of historical candles to fetch from \"\n                              \"exchanges when OctoBot is starting.\")\n        self.social_evaluators_default_timeout = \\\n            self.UI.user_input(self.SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY, commons_enums.UserInputTypes.INT,\n                               default_config[self.SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY],\n                               inputs, min_val=0,\n                               title=\"Number of seconds to consider a social evaluation valid from the moment it \"\n                                  \"appears on OctoBot. Example: a tweet evaluation.\")\n        self.re_evaluate_TA_when_social_or_realtime_notification = \\\n            self.UI.user_input(self.RE_EVAL_TA_ON_RT_OR_SOCIAL, commons_enums.UserInputTypes.BOOLEAN,\n                            default_config[self.RE_EVAL_TA_ON_RT_OR_SOCIAL], inputs,\n                            title=\"Recompute technical evaluators on real-time evaluator signal: \"\n                                  \"When activated, technical evaluators will be asked to recompute their evaluation \"\n                                  \"based on the current in-construction candle \"\n                                  \"for each new evaluation appearing on social or \"\n                                  \"real-time evaluators. After such an event, this strategy will finalize its \"\n                                  \"evaluation only once this updated technical analyses will be completed. \"\n                                  \"If deactivated, social and real-time evaluations will be taken into account \"\n                                  \"alongside technical analysis results of the last closed candle.\")\n        self.background_social_evaluators = \\\n            self.UI.user_input(self.BACKGROUND_SOCIAL_EVALUATORS, commons_enums.UserInputTypes.MULTIPLE_OPTIONS,\n                               default_config[self.BACKGROUND_SOCIAL_EVALUATORS],\n                               inputs, other_schema_values={\"minItems\": 0, \"uniqueItems\": True},\n                            options=[\"RedditForumEvaluator\", \"TwitterNewsEvaluator\",\n                                     \"TelegramSignalEvaluator\", \"GoogleTrendsEvaluator\"],\n                            title=\"Social evaluator to consider as background evaluators: they won't trigger technical \"\n                                  \"evaluators re-evaluation when updated. Avoiding unnecessary updates increases \"\n                                  \"performances.\")\n\n    @classmethod\n    def get_default_config(cls, time_frames: typing.Optional[list[str]] = None) -> dict:\n        return {\n            evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME: (\n                time_frames or [commons_enums.TimeFrames.ONE_HOUR.value]\n            ),\n            commons_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT: 500,\n            cls.SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY: 1 * commons_constants.HOURS_TO_SECONDS,\n            cls.RE_EVAL_TA_ON_RT_OR_SOCIAL: True,\n            cls.BACKGROUND_SOCIAL_EVALUATORS: [],\n        }\n\n    async def matrix_callback(self,\n                              matrix_id,\n                              evaluator_name,\n                              evaluator_type,\n                              eval_note,\n                              eval_note_type,\n                              exchange_name,\n                              cryptocurrency,\n                              symbol,\n                              time_frame):\n        if symbol is None and cryptocurrency is not None and evaluator_type == evaluators_enums.EvaluatorMatrixTypes.SOCIAL.value:\n            # social evaluators can be cryptocurrency related but not symbol related, wakeup every symbol\n            for available_symbol in matrix.get_available_symbols(matrix_id, exchange_name, cryptocurrency):\n                await self._trigger_evaluation(matrix_id,\n                                               evaluator_name,\n                                               evaluator_type,\n                                               eval_note,\n                                               eval_note_type,\n                                               exchange_name,\n                                               cryptocurrency,\n                                               available_symbol)\n            return\n        else:\n            await self._trigger_evaluation(matrix_id,\n                                           evaluator_name,\n                                           evaluator_type,\n                                           eval_note,\n                                           eval_note_type,\n                                           exchange_name,\n                                           cryptocurrency,\n                                           symbol)\n\n    async def _trigger_evaluation(self,\n                                  matrix_id,\n                                  evaluator_name,\n                                  evaluator_type,\n                                  eval_note,\n                                  eval_note_type,\n                                  exchange_name,\n                                  cryptocurrency,\n                                  symbol):\n        # ensure only start evaluations when technical evaluators have been initialized\n        try:\n            TA_by_timeframe = {\n                available_time_frame: matrix.get_evaluations_by_evaluator(\n                    matrix_id,\n                    exchange_name,\n                    evaluators_enums.EvaluatorMatrixTypes.TA.value,\n                    cryptocurrency,\n                    symbol,\n                    available_time_frame.value,\n                    allow_missing=False,\n                    allowed_values=[commons_constants.START_PENDING_EVAL_NOTE])\n                for available_time_frame in self.strategy_time_frames\n            }\n            # social evaluators by symbol\n            social_evaluations_by_evaluator = matrix.get_evaluations_by_evaluator(matrix_id,\n                                                                                  exchange_name,\n                                                                                  evaluators_enums.EvaluatorMatrixTypes.SOCIAL.value,\n                                                                                  cryptocurrency,\n                                                                                  symbol)\n            # social evaluators by crypto currency\n            social_evaluations_by_evaluator.update(matrix.get_evaluations_by_evaluator(matrix_id,\n                                                                                       exchange_name,\n                                                                                       evaluators_enums.EvaluatorMatrixTypes.SOCIAL.value,\n                                                                                       cryptocurrency))\n            available_rt_time_frames = self.get_available_time_frames(matrix_id,\n                                                                      exchange_name,\n                                                                      evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value,\n                                                                      cryptocurrency,\n                                                                      symbol)\n            RT_evaluations_by_time_frame = {\n                available_time_frame: matrix.get_evaluations_by_evaluator(\n                    matrix_id,\n                    exchange_name,\n                    evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value,\n                    cryptocurrency,\n                    symbol,\n                    available_time_frame)\n                for available_time_frame in available_rt_time_frames\n            }\n            if self.re_evaluate_TA_when_social_or_realtime_notification \\\n                    and any(value for value in TA_by_timeframe.values()) \\\n                    and evaluator_type != evaluators_enums.EvaluatorMatrixTypes.TA.value \\\n                    and evaluator_type in self.re_evaluation_triggering_eval_types \\\n                    and evaluator_name not in self.background_social_evaluators:\n                if evaluators_util.check_valid_eval_note(eval_note, eval_type=eval_note_type,\n                                                         expected_eval_type=evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE):\n                    # trigger re-evaluation\n                    exchange_id = trading_api.get_exchange_id_from_matrix_id(exchange_name, matrix_id)\n                    await evaluators_channel.trigger_technical_evaluators_re_evaluation_with_updated_data(matrix_id,\n                                                                                                          evaluator_name,\n                                                                                                          evaluator_type,\n                                                                                                          exchange_name,\n                                                                                                          cryptocurrency,\n                                                                                                          symbol,\n                                                                                                          exchange_id,\n                                                                                                          self.strategy_time_frames)\n                    # do not continue this evaluation\n                    return\n            counter = 0\n            total_evaluation = 0\n\n            for eval_by_rt in RT_evaluations_by_time_frame.values():\n                for evaluation in eval_by_rt.values():\n                    eval_value = evaluators_api.get_value(evaluation)\n                    if evaluators_util.check_valid_eval_note(eval_value, eval_type=evaluators_api.get_type(evaluation),\n                                                             expected_eval_type=evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE):\n                        total_evaluation += eval_value\n                        counter += 1\n\n            for eval_by_ta in TA_by_timeframe.values():\n                for evaluation in eval_by_ta.values():\n                    eval_value = evaluators_api.get_value(evaluation)\n                    if evaluators_util.check_valid_eval_note(eval_value, eval_type=evaluators_api.get_type(evaluation),\n                                                             expected_eval_type=evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE):\n                        total_evaluation += eval_value\n                        counter += 1\n\n            if social_evaluations_by_evaluator:\n                exchange_manager = trading_api.get_exchange_manager_from_exchange_name_and_id(\n                    exchange_name,\n                    trading_api.get_exchange_id_from_matrix_id(exchange_name, self.matrix_id)\n                )\n                current_time = trading_api.get_exchange_current_time(exchange_manager)\n                for evaluation in social_evaluations_by_evaluator.values():\n                    eval_value = evaluators_api.get_value(evaluation)\n                    if evaluators_util.check_valid_eval_note(eval_value, eval_type=evaluators_api.get_type(evaluation),\n                                                             expected_eval_type=evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE,\n                                                             eval_time=evaluators_api.get_time(evaluation),\n                                                             expiry_delay=self.social_evaluators_default_timeout,\n                                                             current_time=current_time):\n                        total_evaluation += eval_value\n                        counter += 1\n\n            if counter > 0:\n                self.eval_note = total_evaluation / counter\n                await self.strategy_completed(cryptocurrency, symbol)\n\n        except errors.UnsetTentacleEvaluation as e:\n            if evaluator_type == evaluators_enums.EvaluatorMatrixTypes.TA.value:\n                self.logger.error(f\"Missing technical evaluator data for ({e})\")\n            # otherwise it's a social or real-time evaluator, it will shortly be taken into account by TA update cycle\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when computing strategy evaluation: {e}\")\n\n\nclass TechnicalAnalysisStrategyEvaluator(evaluators.StrategyEvaluator):\n    TIME_FRAMES_TO_WEIGHT = \"time_frames_to_weight\"\n    TIME_FRAME = \"time_frame\"\n    WEIGHT = \"weight\"\n    DEFAULT_WEIGHT = 50\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.allowed_evaluator_types = [evaluators_enums.EvaluatorMatrixTypes.TA.value,\n                                        evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value]\n        config = tentacles_manager_api.get_tentacle_config(self.tentacles_setup_config, self.__class__)\n        if config:\n            self.weight_by_time_frames = TechnicalAnalysisStrategyEvaluator._get_weight_by_time_frames(\n                config[TechnicalAnalysisStrategyEvaluator.TIME_FRAMES_TO_WEIGHT]\n            )\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        super().init_user_inputs(inputs)\n        time_frames_and_weight = []\n        config_time_frames_and_weight = self.UI.user_input(\n            self.TIME_FRAMES_TO_WEIGHT, commons_enums.UserInputTypes.OBJECT_ARRAY,\n            time_frames_and_weight, inputs, other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n            item_title=\"Time frame\",\n            title=\"Analysed time frames and their associated weight.\"\n        )\n        # init one user input to generate user input schema and default values\n        time_frames_and_weight.append(self._init_tf_and_weight(inputs, commons_enums.TimeFrames.THIRTY_MINUTES, 30))\n        self.weight_by_time_frames = TechnicalAnalysisStrategyEvaluator._get_weight_by_time_frames(\n            config_time_frames_and_weight\n        )\n\n    def _init_tf_and_weight(self, inputs, timeframe, weight):\n        return {\n            self.TIME_FRAME: self.UI.user_input(self.TIME_FRAME, commons_enums.UserInputTypes.OPTIONS,\n                                             timeframe.value, inputs,\n                                             options=[tf.value for tf in commons_enums.TimeFrames],\n                                             parent_input_name=self.TIME_FRAMES_TO_WEIGHT,\n                                             array_indexes=[0],\n                                             title=\"Time frame\"),\n            self.WEIGHT: self.UI.user_input(self.WEIGHT, commons_enums.UserInputTypes.FLOAT,\n                                         weight, inputs, min_val=0, max_val=100,\n                                         parent_input_name=self.TIME_FRAMES_TO_WEIGHT,\n                                         array_indexes=[0],\n                                         title=\"Weight of this time frame. This is a multiplier: 0 means this time \"\n                                               \"frame is ignored, 100 means it's 100 times more impactful than another \"\n                                               \"time frame with a weight of 1.\"),\n        }\n\n    async def matrix_callback(self,\n                              matrix_id,\n                              evaluator_name,\n                              evaluator_type,\n                              eval_note,\n                              eval_note_type,\n                              exchange_name,\n                              cryptocurrency,\n                              symbol,\n                              time_frame):\n        if evaluator_type not in self.allowed_evaluator_types:\n            # only wake up on relevant callbacks\n            return\n\n        try:\n            TA_by_timeframe = {\n                available_time_frame: matrix.get_evaluations_by_evaluator(\n                    matrix_id,\n                    exchange_name,\n                    evaluators_enums.EvaluatorMatrixTypes.TA.value,\n                    cryptocurrency,\n                    symbol,\n                    available_time_frame.value,\n                    allow_missing=False,\n                    allowed_values=[commons_constants.START_PENDING_EVAL_NOTE])\n                for available_time_frame in self.strategy_time_frames\n            }\n\n            if evaluator_type == evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value:\n                # trigger re-evaluation\n                exchange_id = trading_api.get_exchange_id_from_matrix_id(exchange_name, matrix_id)\n                await evaluators_channel.trigger_technical_evaluators_re_evaluation_with_updated_data(matrix_id,\n                                                                                                      evaluator_name,\n                                                                                                      evaluator_type,\n                                                                                                      exchange_name,\n                                                                                                      cryptocurrency,\n                                                                                                      symbol,\n                                                                                                      exchange_id,\n                                                                                                      self.strategy_time_frames)\n                # do not continue this evaluation\n                return\n\n            total_evaluation = 0\n            total_weights = 0\n\n            for time_frame, eval_by_ta in TA_by_timeframe.items():\n                for evaluation in eval_by_ta.values():\n                    eval_value = evaluators_api.get_value(evaluation)\n                    if evaluators_util.check_valid_eval_note(eval_value, eval_type=evaluators_api.get_type(evaluation),\n                                                             expected_eval_type=evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE):\n                        weight = self.weight_by_time_frames.get(time_frame.value, self.DEFAULT_WEIGHT)\n                        total_evaluation += eval_value * weight\n                        total_weights += weight\n\n            if total_weights > 0:\n                self.eval_note = total_evaluation / total_weights\n                await self.strategy_completed(cryptocurrency, symbol)\n\n        except errors.UnsetTentacleEvaluation as e:\n            self.logger.error(f\"Missing technical evaluator data for ({e})\")\n\n    @staticmethod\n    def _get_weight_by_time_frames(tf_to_weight):\n        return {\n            tf_and_weight[TechnicalAnalysisStrategyEvaluator.TIME_FRAME]:\n                tf_and_weight[TechnicalAnalysisStrategyEvaluator.WEIGHT]\n            for tf_and_weight in tf_to_weight\n        }\n"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/resources/SimpleStrategyEvaluator.md",
    "content": "SimpleStrategyEvaluator is the most flexible strategy. Meant to be customized, it is using\nevery activated technical, social and real time evaluator, and averages the evaluation value of\neach to compute its final evaluation.\n\nThis strategy can be used to make trading signals using as many evaluators as required.\n\nUsed time frames are 1h, 4h and 1d by default.\n\nWarning: this strategy only considers evaluators with evaluations values between -1 and 1.\n"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/resources/TechnicalAnalysisStrategyEvaluator.md",
    "content": "TechnicalAnalysisStrategyEvaluator a flexible technical analysis strategy. Meant to be customized, it is using \nevery activated technical evaluator and averages the evaluation value of each to compute its final evaluation. \n\nThis strategy makes it possible to assign a weight to any time frame in order to make the related technical evaluations \nmore or less impactful for the final strategy evaluation. If not specified for a time frame, default weight is 50.\n\nThis strategy can be used to create custom trading signals using as many technical \nevaluators as desired.\n\nTechnicalAnalysisStrategyEvaluator can also use real time evaluators to trigger an instant re-evaluation of its technical \nevaluators and react quickly. The evaluation value of these real time evaluators will not be considered in the final strategy \nevaluation as they are only meant to trigger an emergency re-evaluation.\n\nUsed time frames are 30m, 1h, 2h, 4h and 1d by default.\n\nWarning: this strategy only considers evaluators with evaluations values between -1 and 1.\n"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/tests/test_simple_strategy_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport pytest\n\nimport tests.functional_tests.strategy_evaluators_tests.abstract_strategy_test as abstract_strategy_test\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Trading.Mode as Mode\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest.fixture\ndef strategy_tester():\n    strategy_tester_instance = SimpleStrategyEvaluatorTest()\n    strategy_tester_instance.initialize(Strategies.SimpleStrategyEvaluator, Mode.DailyTradingMode)\n    return strategy_tester_instance\n\n\nclass SimpleStrategyEvaluatorTest(abstract_strategy_test.AbstractStrategyTest):\n    \"\"\"\n    About using this test framework:\n    To be called by pytest, tests have to be called manually since the cythonized version of AbstractStrategyTest\n    creates an __init__() which prevents the default pytest tests collect process\n    \"\"\"\n\n    async def test_default_run(self):\n        # market: -13.599062133645944\n        await self.run_test_default_run(decimal.Decimal(str(-1.090)))\n\n    async def test_slow_downtrend(self):\n        # market: -13.599062133645944\n        # market: -44.248234106962656\n        # market: -34.87003936300901\n        # market: -45.18518518518518\n        await self.run_test_slow_downtrend(decimal.Decimal(str(-1.090)), decimal.Decimal(str(-36.523)),\n                                           decimal.Decimal(str(-27.337)), decimal.Decimal(str(-31.155)))\n\n    async def test_sharp_downtrend(self):\n        # market: -30.271723049610415\n        # market: -32.091097308488614\n        await self.run_test_sharp_downtrend(decimal.Decimal(str(-24.356)), decimal.Decimal(str(-32.781)))\n\n    async def test_flat_markets(self):\n        # market: 5.052093571849795\n        # market: 3.4840425531915002\n        # market: -12.732688011913623\n        # market: -34.64150943396227\n        await self.run_test_flat_markets(decimal.Decimal(str(0.027)), decimal.Decimal(str(11.215)),\n                                         decimal.Decimal(str(-13.888)), decimal.Decimal(str(-4.472)))\n\n    async def test_slow_uptrend(self):\n        # market: 32.524679029957184\n        # market: 6.25\n        await self.run_test_slow_uptrend(decimal.Decimal(str(15.031)), decimal.Decimal(str(0.831)))\n\n    async def test_sharp_uptrend(self):\n        # market: 24.56254050550875\n        # market: 8.665472458575891\n        await self.run_test_sharp_uptrend(decimal.Decimal(str(14.212)), decimal.Decimal(str(13.007)))\n\n    async def test_up_then_down(self):\n        # market: 1.1543668450702853\n        await self.run_test_up_then_down(decimal.Decimal(str(2.674)))\n\n\nasync def test_default_run(strategy_tester):\n    await strategy_tester.test_default_run()\n\n\nasync def test_slow_downtrend(strategy_tester):\n    await strategy_tester.test_slow_downtrend()\n\n\nasync def test_sharp_downtrend(strategy_tester):\n    await strategy_tester.test_sharp_downtrend()\n\n\nasync def test_flat_markets(strategy_tester):\n    await strategy_tester.test_flat_markets()\n\n\nasync def test_slow_uptrend(strategy_tester):\n    await strategy_tester.test_slow_uptrend()\n\n\nasync def test_sharp_uptrend(strategy_tester):\n    await strategy_tester.test_sharp_uptrend()\n\n\nasync def test_up_then_down(strategy_tester):\n    await strategy_tester.test_up_then_down()\n"
  },
  {
    "path": "Evaluator/Strategies/mixed_strategies_evaluator/tests/test_technical_analysis_strategy_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport pytest\n\nimport tests.functional_tests.strategy_evaluators_tests.abstract_strategy_test as abstract_strategy_test\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Trading.Mode as Mode\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest.fixture\ndef strategy_tester():\n    strategy_tester_instance = TechnicalAnalysisStrategyEvaluatorTest()\n    strategy_tester_instance.initialize(Strategies.TechnicalAnalysisStrategyEvaluator, Mode.DailyTradingMode)\n    return strategy_tester_instance\n\n\nclass TechnicalAnalysisStrategyEvaluatorTest(abstract_strategy_test.AbstractStrategyTest):\n    \"\"\"\n    About using this test framework:\n    To be called by pytest, tests have to be called manually since the cythonized version of AbstractStrategyTest\n    creates an __init__() which prevents the default pytest tests collect process\n    \"\"\"\n\n    async def test_default_run(self):\n        # market: -12.052505966587105\n        await self.run_test_default_run(decimal.Decimal(str(-8.699)))\n\n    async def test_slow_downtrend(self):\n        # market: -12.052505966587105\n        # market: -15.195702225633141\n        # market: -29.12366137549725\n        # market: -32.110091743119256\n        await self.run_test_slow_downtrend(decimal.Decimal(str(-8.699)), decimal.Decimal(str(-9.671)),\n                                           decimal.Decimal(str(-16.968)), decimal.Decimal(str(-7.236)))\n\n    async def test_sharp_downtrend(self):\n        # market: -26.07183938094741\n        # market: -32.1654501216545\n        await self.run_test_sharp_downtrend(decimal.Decimal(str(-19.903)), decimal.Decimal(str(-23.076)))\n\n    async def test_flat_markets(self):\n        # market: -10.560669456066947\n        # market: -3.401191658391241\n        # market: -5.7854560064282765\n        # market: -8.067940552016978\n        await self.run_test_flat_markets(decimal.Decimal(str(0.289)), decimal.Decimal(str(1.813)),\n                                         decimal.Decimal(str(-4.596)), decimal.Decimal(str(3.884)))\n\n    async def test_slow_uptrend(self):\n        # market: 17.203948364436457\n        # market: 16.19613670133728\n        await self.run_test_slow_uptrend(decimal.Decimal(str(8.245)), decimal.Decimal(str(2.882)))\n\n    async def test_sharp_uptrend(self):\n        # market: 30.881852230166828\n        # market: 12.28597871355852\n        await self.run_test_sharp_uptrend(decimal.Decimal(str(1.418)), decimal.Decimal(str(4.362)))\n\n    async def test_up_then_down(self):\n        # market: -6.040105108015155\n        await self.run_test_up_then_down(decimal.Decimal(str(-0.964)))\n\n\nasync def test_default_run(strategy_tester):\n    await strategy_tester.test_default_run()\n\n\nasync def test_slow_downtrend(strategy_tester):\n    await strategy_tester.test_slow_downtrend()\n\n\nasync def test_sharp_downtrend(strategy_tester):\n    await strategy_tester.test_sharp_downtrend()\n\n\nasync def test_flat_markets(strategy_tester):\n    await strategy_tester.test_flat_markets()\n\n\nasync def test_slow_uptrend(strategy_tester):\n    await strategy_tester.test_slow_uptrend()\n\n\nasync def test_sharp_uptrend(strategy_tester):\n    await strategy_tester.test_sharp_uptrend()\n\n\nasync def test_up_then_down(strategy_tester):\n    await strategy_tester.test_up_then_down()\n"
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/__init__.py",
    "content": "from .move_signals_strategy import MoveSignalsStrategyEvaluator"
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/config/MoveSignalsStrategyEvaluator.json",
    "content": "{\n  \"required_time_frames\" : [\"30m\", \"1h\", \"4h\"],\n  \"required_evaluators\" : [\"InstantFluctuationsEvaluator\", \"KlingerOscillatorMomentumEvaluator\", \"BBMomentumEvaluator\"],\n  \"default_config\" : [\"KlingerOscillatorMomentumEvaluator\", \"BBMomentumEvaluator\"]\n}"
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"MoveSignalsStrategyEvaluator\"],\n  \"tentacles-requirements\": [\"momentum_evaluator.py\"]\n}"
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/move_signals_strategy.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enum\nimport octobot_evaluators.api.matrix as evaluators_api\nimport octobot_evaluators.evaluators.channel as evaluators_channel\nimport octobot_evaluators.matrix as matrix\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_evaluators.errors as errors\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_trading.api as trading_api\nimport tentacles.Evaluator.TA as TA\n\n\nclass MoveSignalsStrategyEvaluator(evaluators.StrategyEvaluator):\n    SIGNAL_CLASS_NAME = TA.KlingerOscillatorMomentumEvaluator.get_name()\n    WEIGHT_CLASS_NAME = TA.BBMomentumEvaluator.get_name()\n\n    SHORT_PERIOD_WEIGHT = 4\n    MEDIUM_PERIOD_WEIGHT = 3\n    LONG_PERIOD_WEIGHT = 3\n\n    SIGNAL_MINIMUM_THRESHOLD = 0.15\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.evaluation_time_frames = [commons_enum.TimeFrames.THIRTY_MINUTES.value,\n                                       commons_enum.TimeFrames.ONE_HOUR.value,\n                                       commons_enum.TimeFrames.FOUR_HOURS.value]\n        self.weights_and_period_evals = []\n        self.short_period_eval = None\n        self.medium_period_eval = None\n        self.long_period_eval = None\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        pass\n\n    async def matrix_callback(self,\n                              matrix_id,\n                              evaluator_name,\n                              evaluator_type,\n                              eval_note,\n                              eval_note_type,\n                              exchange_name,\n                              cryptocurrency,\n                              symbol,\n                              time_frame):\n        if evaluator_type == evaluators_enums.EvaluatorMatrixTypes.REAL_TIME.value:\n            # trigger re-evaluation\n            exchange_id = trading_api.get_exchange_id_from_matrix_id(exchange_name, matrix_id)\n            await evaluators_channel.trigger_technical_evaluators_re_evaluation_with_updated_data(matrix_id,\n                                                                                                  evaluator_name,\n                                                                                                  evaluator_type,\n                                                                                                  exchange_name,\n                                                                                                  cryptocurrency,\n                                                                                                  symbol,\n                                                                                                  exchange_id,\n                                                                                                  self.strategy_time_frames)\n            # do not continue this evaluation\n            return\n        elif evaluator_type == evaluators_enums.EvaluatorMatrixTypes.TA.value:\n            try:\n                TA_by_timeframe = {\n                    available_time_frame: matrix.get_evaluations_by_evaluator(\n                        matrix_id,\n                        exchange_name,\n                        evaluators_enums.EvaluatorMatrixTypes.TA.value,\n                        cryptocurrency,\n                        symbol,\n                        available_time_frame.value,\n                        allow_missing=False,\n                        allowed_values=[commons_constants.START_PENDING_EVAL_NOTE])\n                    for available_time_frame in self.strategy_time_frames\n                }\n\n                self._refresh_evaluations(TA_by_timeframe)\n                self._compute_final_evaluation()\n                await self.strategy_completed(cryptocurrency, symbol)\n\n            except errors.UnsetTentacleEvaluation as e:\n                self.logger.debug(f\"Tentacles evaluation initialization: not ready yet for a strategy update ({e})\")\n            except KeyError as e:\n                self.logger.exception(e, True, f\"Missing {e} evaluation in matrix for {symbol} on {time_frame}, \"\n                                      f\"did you activate the required evaluator ?\")\n\n    def _compute_final_evaluation(self):\n        weights = 0\n        composite_evaluation = 0\n        for weight, evaluation in self.weights_and_period_evals:\n            composite_evaluation += self._compute_fractal_evaluation(evaluation, weight)\n            weights += weight\n        self.eval_note = composite_evaluation / weights\n\n    @staticmethod\n    def _compute_fractal_evaluation(signal_with_weight, multiplier):\n        if signal_with_weight.signal != commons_constants.START_PENDING_EVAL_NOTE \\\n                and signal_with_weight.weight != commons_constants.START_PENDING_EVAL_NOTE:\n            evaluation_sign = signal_with_weight.signal * signal_with_weight.weight\n            if abs(signal_with_weight.signal) >= MoveSignalsStrategyEvaluator.SIGNAL_MINIMUM_THRESHOLD \\\n                    and evaluation_sign > 0:\n                eval_side = 1 if signal_with_weight.signal > 0 else -1\n                signal_strength = 2 * signal_with_weight.signal * signal_with_weight.weight\n                weighted_eval = min(signal_strength, 1)\n                return weighted_eval * multiplier * eval_side\n        return 0\n\n    def _refresh_evaluations(self, TA_by_timeframe):\n        for _, evaluation in self.weights_and_period_evals:\n            evaluation.refresh_evaluation(TA_by_timeframe)\n\n    def _get_tentacle_registration_topic(self, all_symbols_by_crypto_currencies, time_frames, real_time_time_frames):\n        currencies, symbols, time_frames = super()._get_tentacle_registration_topic(all_symbols_by_crypto_currencies,\n                                                                                    time_frames,\n                                                                                    real_time_time_frames)\n        # register evaluation fractals based on available time frames\n        self._register_time_frame(commons_enum.TimeFrames.THIRTY_MINUTES, self.SHORT_PERIOD_WEIGHT)\n        self._register_time_frame(commons_enum.TimeFrames.ONE_HOUR, self.MEDIUM_PERIOD_WEIGHT)\n        self._register_time_frame(commons_enum.TimeFrames.FOUR_HOURS, self.LONG_PERIOD_WEIGHT)\n        return currencies, symbols, time_frames\n\n    def _register_time_frame(self, time_frame, weight):\n        if time_frame in self.strategy_time_frames:\n            self.weights_and_period_evals.append((weight,\n                                                  SignalWithWeight(time_frame)))\n        else:\n            self.logger.warning(f\"Missing {time_frame.value} time frame on {self.exchange_name}, \"\n                                f\"this strategy will not work at its optimal potential.\")\n\n\nclass SignalWithWeight:\n\n    def __init__(self, time_frame):\n        self.time_frame = time_frame\n        self.signal = commons_constants.START_PENDING_EVAL_NOTE\n        self.weight = commons_constants.START_PENDING_EVAL_NOTE\n\n    def reset_evaluation(self):\n        self.signal = commons_constants.START_PENDING_EVAL_NOTE\n        self.weight = commons_constants.START_PENDING_EVAL_NOTE\n\n    def refresh_evaluation(self, TA_by_timeframe):\n        self.reset_evaluation()\n        self.signal = evaluators_api.get_value(\n            TA_by_timeframe[self.time_frame][MoveSignalsStrategyEvaluator.SIGNAL_CLASS_NAME])\n        self.weight = evaluators_api.get_value(\n            TA_by_timeframe[self.time_frame][MoveSignalsStrategyEvaluator.WEIGHT_CLASS_NAME])\n"
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/resources/MoveSignalsStrategyEvaluator.md",
    "content": "MoveSignalsStrategyEvaluator is a fractal strategy: it is using different time frames to\nbalance decisions. \n\nThis strategy is using the KlingerOscillatorMomentumEvaluator based on the [Klinger Oscillator](https://www.investopedia.com/terms/k/klingeroscillator.asp)\nto know when to start a trade and BBMomentumEvaluator based on [Bollinger Bands](https://www.investopedia.com/terms/b/bollingerbands.asp)\nto know how much weight to give to this trade. \n\nThis strategy is updated at the end of each candle on the watched time frame which is each 30 minutes. \n\nIt is also possible to make it trigger \nautomatically using a real-time evaluator. Using a real time evaluator that signals sudden market changes like the \nInstantFluctuationsEvaluator will make MoveSignalsStrategyEvaluator also wake up on such events.\n\nUsed time frames are 30m, 1h and 4h. \n\nWarning: MoveSignalsStrategyEvaluator only works on liquid markets because the Klinger Oscillator requires enough \nvolume and candles continuity to be accurate."
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Evaluator/Strategies/move_signals_strategy_evaluator/tests/test_move_signals_strategy_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport pytest\n\nimport tests.functional_tests.strategy_evaluators_tests.abstract_strategy_test as abstract_strategy_test\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Trading.Mode as Mode\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest.fixture\ndef strategy_tester():\n    strategy_tester_instance = MoveSignalsStrategyEvaluatorTest()\n    strategy_tester_instance.initialize(Strategies.MoveSignalsStrategyEvaluator, Mode.SignalTradingMode)\n    return strategy_tester_instance\n\n\nclass MoveSignalsStrategyEvaluatorTest(abstract_strategy_test.AbstractStrategyTest):\n    \"\"\"\n    About using this test framework:\n    To be called by pytest, tests have to be called manually since the cythonized version of AbstractStrategyTest\n    creates an __init__() which prevents the default pytest tests collect process\n    \"\"\"\n\n    async def test_default_run(self):\n        # market: -12.052505966587105\n        await self.run_test_default_run(decimal.Decimal(str(-2.549)))\n\n    async def test_slow_downtrend(self):\n        # market: -12.052505966587105\n        # market: -15.195702225633141\n        # market: -29.12366137549725\n        # market: -32.110091743119256\n        await self.run_test_slow_downtrend(decimal.Decimal(str(-2.549)), decimal.Decimal(str(-3.452)),\n                                           decimal.Decimal(str(-17.393)), decimal.Decimal(str(-15.761)))\n\n    async def test_sharp_downtrend(self):\n        # market: -26.07183938094741\n        # market: -32.1654501216545\n        await self.run_test_sharp_downtrend(decimal.Decimal(str(-12.078)), decimal.Decimal(str(-10.3)))\n\n    async def test_flat_markets(self):\n        # market: -10.560669456066947\n        # market: -3.401191658391241\n        # market: -5.7854560064282765\n        # market: -8.067940552016978\n        await self.run_test_flat_markets(decimal.Decimal(str(-0.200)), decimal.Decimal(str(0.353)),\n                                         decimal.Decimal(str(-8.126)), decimal.Decimal(str(-7.038)))\n\n    async def test_slow_uptrend(self):\n        # market: 17.203948364436457\n        # market: 16.19613670133728\n        await self.run_test_slow_uptrend(decimal.Decimal(str(10.278)), decimal.Decimal(str(4.299)))\n\n    async def test_sharp_uptrend(self):\n        # market: 30.881852230166828\n        # market: 12.28597871355852\n        await self.run_test_sharp_uptrend(decimal.Decimal(str(6.504)), decimal.Decimal(str(5.411)))\n\n    async def test_up_then_down(self):\n        # market: -6.040105108015155\n        await self.run_test_up_then_down(decimal.Decimal(str(-6.691)))\n\n\nasync def test_default_run(strategy_tester):\n    await strategy_tester.test_default_run()\n\n\nasync def test_slow_downtrend(strategy_tester):\n    await strategy_tester.test_slow_downtrend()\n\n\nasync def test_sharp_downtrend(strategy_tester):\n    await strategy_tester.test_sharp_downtrend()\n\n\nasync def test_flat_markets(strategy_tester):\n    await strategy_tester.test_flat_markets()\n\n\nasync def test_slow_uptrend(strategy_tester):\n    await strategy_tester.test_slow_uptrend()\n\n\nasync def test_sharp_uptrend(strategy_tester):\n    await strategy_tester.test_sharp_uptrend()\n\n\nasync def test_up_then_down(strategy_tester):\n    await strategy_tester.test_up_then_down()\n"
  },
  {
    "path": "Evaluator/TA/ai_evaluator/__init__.py",
    "content": "from .ai import GPTEvaluator"
  },
  {
    "path": "Evaluator/TA/ai_evaluator/ai.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tulipy\nimport os\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.enums as enums\nimport octobot_commons.os_util as os_util\nimport octobot_commons.data_util as data_util\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.util as evaluators_util\nimport octobot_evaluators.errors as evaluators_errors\nimport octobot_trading.api as trading_api\nimport octobot_services.api as services_api\nimport octobot_services.errors as services_errors\nimport tentacles.Services.Services_bases\n\n\ndef _get_gpt_service():\n    try:\n        return tentacles.Services.Services_bases.GPTService\n    except (AttributeError, ImportError):\n        raise ImportError(\"the gpt_service tentacle is not installed\")\n\n\nclass GPTEvaluator(evaluators.TAEvaluator):\n    GLOBAL_VERSION = 1\n    PREPROMPT = \"Predict: {up or down} {confidence%} (no other information)\"\n    PASSED_DATA_LEN = 10\n    MAX_CONFIDENCE_PERCENT = 100\n    HIGH_CONFIDENCE_PERCENT = 80\n    MEDIUM_CONFIDENCE_PERCENT = 50\n    LOW_CONFIDENCE_PERCENT = 30\n    INDICATORS = {\n        \"No indicator: raw candles price data\": lambda data, period: data,\n        \"EMA: Exponential Moving Average\": tulipy.ema,\n        \"SMA: Simple Moving Average\": tulipy.sma,\n        \"Kaufman Adaptive Moving Average\": tulipy.kama,\n        \"Hull Moving Average\": tulipy.kama,\n        \"RSI: Relative Strength Index\": tulipy.rsi,\n        \"Detrended Price Oscillator\": tulipy.dpo,\n    }\n    SOURCES = [\"Open\", \"High\", \"Low\", \"Close\", \"Volume\", \"Full candle (For no indicator only)\"]\n    ALLOW_GPT_REEVALUATION_ENV = \"ALLOW_GPT_REEVALUATIONS\"\n    GPT_MODELS = []\n    ALLOW_TOKEN_LIMIT_UPDATE = False\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.indicator = None\n        self.source = None\n        self.period = None\n        self.min_confidence_threshold = 100\n        self.gpt_model = _get_gpt_service().DEFAULT_MODEL\n        self.is_backtesting = False\n        self.min_allowed_timeframe = os.getenv(\"MIN_GPT_TIMEFRAME\", None)\n        self.enable_model_selector = os_util.parse_boolean_environment_var(\"ENABLE_GPT_MODELS_SELECTOR\", \"True\")\n        self._min_allowed_timeframe_minutes = 0\n        try:\n            if self.min_allowed_timeframe:\n                self._min_allowed_timeframe_minutes = \\\n                    commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(self.min_allowed_timeframe)]\n        except ValueError:\n            self.logger.error(f\"Invalid timeframe configuration: unknown timeframe: '{self.min_allowed_timeframe}'\")\n        self.allow_reevaluations = os_util.parse_boolean_environment_var(self.ALLOW_GPT_REEVALUATION_ENV, \"True\")\n        self.gpt_tokens_limit = _get_gpt_service().NO_TOKEN_LIMIT_VALUE\n        self.services_config = None\n\n    def enable_reevaluation(self) -> bool:\n        \"\"\"\n        Override when artificial re-evaluations from the evaluator channel can be disabled\n        \"\"\"\n        return self.allow_reevaluations\n\n    @classmethod\n    def get_signals_history_type(cls):\n        \"\"\"\n        Override when this evaluator uses a specific type of signal history\n        \"\"\"\n        return commons_enums.SignalHistoryTypes.GPT\n\n    async def load_and_save_user_inputs(self, bot_id: str) -> dict:\n        \"\"\"\n        instance method API for user inputs\n        Initialize and save the tentacle user inputs in run data\n        :return: the filled user input configuration\n        \"\"\"\n        self.is_backtesting = self._is_in_backtesting()\n        if self.is_backtesting and not _get_gpt_service().BACKTESTING_ENABLED:\n            self.logger.error(f\"{self.get_name()} is disabled in backtesting. It will only emit neutral evaluations\")\n        await self._init_GPT_models()\n        return await super().load_and_save_user_inputs(bot_id)\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.indicator = self.UI.user_input(\n            \"indicator\", enums.UserInputTypes.OPTIONS, next(iter(self.INDICATORS)),\n            inputs, options=list(self.INDICATORS),\n            title=\"Indicator: the technical indicator to apply and give the result of to chat GPT.\"\n        )\n        self.source = self.UI.user_input(\n            \"source\", enums.UserInputTypes.OPTIONS, self.SOURCES[3],\n            inputs, options=self.SOURCES,\n            title=\"Source: values of candles data to pass to the indicator.\"\n        )\n        self.period = self.UI.user_input(\n            \"period\", enums.UserInputTypes.INT,\n            self.period, inputs, min_val=1,\n            title=\"Period: length of the indicator period or the number of candles to give to ChatGPT.\"\n        )\n        self.min_confidence_threshold = self.UI.user_input(\n            \"min_confidence_threshold\", enums.UserInputTypes.INT,\n            self.min_confidence_threshold, inputs, min_val=0, max_val=100,\n            title=\"Minimum confidence threshold: % confidence value starting from which to return 1 or -1.\"\n        )\n        if self.enable_model_selector:\n            current_value = self.specific_config.get(\"GPT_model\")\n            models = list(self.GPT_MODELS) or (\n                [current_value] if current_value else [_get_gpt_service().DEFAULT_MODEL]\n            )\n            self.gpt_model = self.UI.user_input(\n                \"GPT model\", enums.UserInputTypes.OPTIONS, _get_gpt_service().DEFAULT_MODEL,\n                inputs, options=sorted(models),\n                title=\"GPT Model: the GPT model to use. Enable the evaluator to load other models.\"\n            )\n        if os_util.parse_boolean_environment_var(self.ALLOW_GPT_REEVALUATION_ENV, \"True\"):\n            self.allow_reevaluations = self.UI.user_input(\n                \"allow_reevaluation\", enums.UserInputTypes.BOOLEAN, self.allow_reevaluations,\n                inputs,\n                title=\"Allow Reevaluation: send a ChatGPT request when realtime evaluators trigger a \"\n                      \"global reevaluation Use latest available value otherwise. \"\n                      \"Warning: enabling this can lead to a large amount of GPT requests and consumed tokens.\"\n            )\n        if self.ALLOW_TOKEN_LIMIT_UPDATE:\n            self.gpt_tokens_limit = self.UI.user_input(\n                \"max_gpt_tokens\", enums.UserInputTypes.INT,\n                self.gpt_tokens_limit, inputs, min_val=_get_gpt_service().NO_TOKEN_LIMIT_VALUE,\n                title=f\"OpenAI token limit: maximum daily number of tokens to consume with a given OctoBot instance. \"\n                      f\"Use {_get_gpt_service().NO_TOKEN_LIMIT_VALUE} to remove the limit.\"\n            )\n\n    async def _init_GPT_models(self):\n        if not self.GPT_MODELS:\n            self.GPT_MODELS = [_get_gpt_service().DEFAULT_MODEL]\n            if self.enable_model_selector and not self.is_backtesting:\n                try:\n                    service = await services_api.get_service(\n                        _get_gpt_service(), self.is_backtesting, self.services_config\n                    )\n                    self.GPT_MODELS = service.models\n                    self.ALLOW_TOKEN_LIMIT_UPDATE = service.allow_token_limit_update()\n                except Exception as err:\n                    self.logger.exception(err, True, f\"Impossible to fetch GPT models: {err}\")\n\n    async def _init_registered_topics(self, all_symbols_by_crypto_currencies, currencies, symbols, time_frames):\n        await super()._init_registered_topics(all_symbols_by_crypto_currencies, currencies, symbols, time_frames)\n        for time_frame in time_frames:\n            if not self._check_timeframe(time_frame.value):\n                self.logger.error(f\"{time_frame.value} time frame will be ignored for {self.get_name()} \"\n                                  f\"as {time_frame.value} is not allowed in this configuration. \"\n                                  f\"The shortest allowed time frame is {self.min_allowed_timeframe}. {self.get_name()} \"\n                                  f\"will emit neutral evaluations on this time frame.\")\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = self.get_candles_data(exchange, exchange_id, symbol, time_frame, inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        async with self.async_evaluation():\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n            if self._check_timeframe(time_frame):\n                try:\n                    candle_time = candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n                    computed_data = self.call_indicator(candle_data)\n                    formatted_data = self.get_formatted_data(computed_data)\n                    prediction = await self.ask_gpt(self.PREPROMPT, formatted_data, symbol, time_frame, candle_time) \\\n                        or \"\"\n                    cleaned_prediction = prediction.strip().replace(\"\\n\", \"\").replace(\".\", \"\").lower()\n                    prediction_side = self._parse_prediction_side(cleaned_prediction)\n                    if prediction_side == 0 and not self.is_backtesting:\n                        self.logger.warning(\n                            f\"Ignored ChatGPT answer for {symbol} {time_frame}, answer: '{cleaned_prediction}': \"\n                            f\"missing prediction or % accuracy.\"\n                        )\n                        return\n                    confidence = self._parse_confidence(cleaned_prediction) / 100\n                    self.eval_note = prediction_side * confidence\n                except services_errors.InvalidRequestError as e:\n                    self.logger.error(f\"Invalid GPT request: {e}\")\n                except services_errors.RateLimitError as e:\n                    self.logger.error(f\"Impossible to get ChatGPT evaluation for {symbol} on {time_frame}: \"\n                                      f\"No remaining free tokens for today : {e}. To prevent this, you can reduce the \"\n                                      f\"amount of traded pairs, use larger time frames or increase the maximum \"\n                                      f\"allowed tokens.\")\n                except services_errors.UnavailableInBacktestingError:\n                    # error already logged error for backtesting in use_backtesting_init_timeout\n                    pass\n                except evaluators_errors.UnavailableEvaluatorError as e:\n                    self.logger.exception(e, True, f\"Evaluation error: {e}\")\n                except tulipy.lib.InvalidOptionError as e:\n                    self.logger.warning(\n                        f\"Error when computing {self.indicator} on {self.period} period with {len(candle_data)} \"\n                        f\"candles: {e}\"\n                    )\n                    self.logger.exception(e, False)\n            else:\n                self.logger.debug(f\"Ignored {time_frame} time frame as the shorted allowed time frame is \"\n                                  f\"{self.min_allowed_timeframe}\")\n            await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                            eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                    time_frame=time_frame))\n\n    def get_formatted_data(self, computed_data) -> str:\n        if self.source in self.get_unformated_sources():\n            return str(computed_data)\n        reduced_data = computed_data[-self.PASSED_DATA_LEN:]\n        return \", \".join(str(datum).replace('[', '').replace(']', '') for datum in reduced_data)\n\n    async def ask_gpt(self, preprompt, inputs, symbol, time_frame, candle_time) -> str:\n        try:\n            service = await services_api.get_service(\n                _get_gpt_service(),\n                self.is_backtesting,\n                {} if self.is_backtesting else self.services_config\n            )\n            service.apply_daily_token_limit_if_possible(self.gpt_tokens_limit)\n            model = self.gpt_model if self.enable_model_selector else None\n            resp = await service.get_chat_completion(\n                [\n                    service.create_message(\"system\", preprompt, model=model),\n                    service.create_message(\"user\", inputs, model=model),\n                ],\n                model=model,\n                exchange=self.exchange_name,\n                symbol=symbol,\n                time_frame=time_frame,\n                version=self.get_version(),\n                candle_open_time=candle_time,\n                use_stored_signals=self.is_backtesting\n            )\n            self.logger.info(\n                f\"GPT's answer is '{resp}' for {symbol} on {time_frame} with input: {inputs} \"\n                f\"and candle_time: {candle_time}\"\n            )\n            return resp\n        except services_errors.CreationError as err:\n            raise evaluators_errors.UnavailableEvaluatorError(f\"Impossible to get ChatGPT prediction: {err}\") from err\n\n    def get_version(self):\n        # later on, identify by its specs\n        # return f\"{self.gpt_model}-{self.source}-{self.indicator}-{self.period}-{self.GLOBAL_VERSION}\"\n        return \"0.0.0\"\n\n    def call_indicator(self, candle_data):\n        if self.source in self.get_unformated_sources():\n            return candle_data\n        return data_util.drop_nan(self.INDICATORS[self.indicator](candle_data, self.period))\n\n    def get_candles_data(self, exchange, exchange_id, symbol, time_frame, inc_in_construction_data):\n        if self.source in self.get_unformated_sources():\n            limit = self.period if inc_in_construction_data else self.period + 1\n            full_candles = trading_api.get_candles_as_list(\n                trading_api.get_symbol_historical_candles(\n                    self.get_exchange_symbol_data(exchange, exchange_id, symbol), time_frame, limit=limit\n                )\n            )\n            # remove time value\n            for candle in full_candles:\n                candle.pop(commons_enums.PriceIndexes.IND_PRICE_TIME.value)\n            if inc_in_construction_data:\n                return full_candles\n            return full_candles[:-1]\n        return self.get_candles_data_api()(\n            self.get_exchange_symbol_data(exchange, exchange_id, symbol), time_frame,\n            include_in_construction=inc_in_construction_data\n        )\n\n    def get_unformated_sources(self):\n        return (self.SOURCES[5], )\n\n    def get_candles_data_api(self):\n        return {\n            self.SOURCES[0]: trading_api.get_symbol_open_candles,\n            self.SOURCES[1]: trading_api.get_symbol_high_candles,\n            self.SOURCES[2]: trading_api.get_symbol_low_candles,\n            self.SOURCES[3]: trading_api.get_symbol_close_candles,\n            self.SOURCES[4]: trading_api.get_symbol_volume_candles,\n        }[self.source]\n\n    def _check_timeframe(self, time_frame):\n        return commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(time_frame)] >= \\\n            self._min_allowed_timeframe_minutes\n\n    def _parse_prediction_side(self, cleaned_prediction):\n        if \"down \" in cleaned_prediction:\n            return 1\n        elif \"up \" in cleaned_prediction:\n            return -1\n        return 0\n\n    def _parse_confidence(self, cleaned_prediction):\n        \"\"\"\n        possible formats:\n        up 70%                   (most common case)\n        up with 70% confidence\n        up with high confidence\n        \"\"\"\n        value = self.LOW_CONFIDENCE_PERCENT\n        if \"%\" in cleaned_prediction:\n            percent_index = cleaned_prediction.index(\"%\")\n            bracket_index = (cleaned_prediction[:percent_index].rindex(\"{\") + 1) \\\n                if \"{\" in cleaned_prediction[:percent_index] else 0\n            value = float(cleaned_prediction[bracket_index:percent_index].split(\" \")[-1])\n        elif \"high\" in cleaned_prediction:\n            value = self.HIGH_CONFIDENCE_PERCENT\n        elif \"medium\" in cleaned_prediction or \"intermediate\" in cleaned_prediction:\n            value = self.MEDIUM_CONFIDENCE_PERCENT\n        elif \"low\" in cleaned_prediction:\n            value = self.LOW_CONFIDENCE_PERCENT\n        elif not cleaned_prediction:\n            value = 0\n        else:\n            self.logger.warning(f\"Impossible to parse confidence in {cleaned_prediction}. Using low confidence\")\n        if value >= self.min_confidence_threshold:\n            return self.MAX_CONFIDENCE_PERCENT\n        return value\n"
  },
  {
    "path": "Evaluator/TA/ai_evaluator/config/GPTEvaluator.json",
    "content": "{\n    \"indicator\": \"No indicator: raw candles price data\",\n    \"period\": 2,\n    \"source\": \"Close\",\n    \"min_confidence_threshold\": 100,\n    \"allow_reevaluation\": false,\n    \"max_gpt_tokens\": -1\n}"
  },
  {
    "path": "Evaluator/TA/ai_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GPTEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/TA/ai_evaluator/resources/GPTEvaluator.md",
    "content": "Uses [Chat GPT](https://chat.openai.com/) to predict the next moves of the market.\n\nEvaluates between -1 to 1 according to ChatGPT's prediction of the selected data and its confidence.\n\nLearn more about ChatGPT trading strategies from our \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/chatgpt-trading?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=GPTEvaluator\">\nChatGPT Trading guide</a>.\n\n<div class=\"text-center\">\n    <div>\n\n    <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/P23oiE8gW4Y?si=6p4a25VOx74DB2Kh\" \n    title=\"Build your own Smart DCA strategy\" frameborder=\"0\" allow=\"accelerometer; autoplay; \n    clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n\n\n    </div>\n    Example of a trading strategy using ChatGPT and the ChatGPTEvaluator\n</div>\n\nAny question ? Checkout our [ChatGPT setup guide](https://www.octobot.cloud/en/guides/octobot-interfaces/chatgpt?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=GPTEvaluator) to configure your OctoBot \nto use ChatGPT.\n\nNote: this evaluator can only be used in backtesting for markets where historical ChatGPT data are available.\nFind the full list of supported historical markets on our [ChatGPT page](https://www.octobot.cloud/features/chatgpt-trading?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=GPTEvaluator).\n\n\n"
  },
  {
    "path": "Evaluator/TA/ai_evaluator/tests/test_ai.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport types\nimport mock\nimport pytest\nimport numpy\n\nimport tentacles.Evaluator.TA.ai_evaluator as ai_evaluator\n\n\n@pytest.fixture\ndef GPT_evaluator():\n    return ai_evaluator.GPTEvaluator(mock.Mock(is_tentacle_activated=mock.Mock(return_value=True)))\n\n\ndef test_indicators(GPT_evaluator):\n    data = numpy.array([100, 223, 123, 23, 134, 124, 434, 3243, 121, 3242.34, 1212, 87, 232.32])\n    for indicator in GPT_evaluator.INDICATORS:\n        GPT_evaluator.indicator = indicator\n        GPT_evaluator.period = 2\n        assert len(data) - (GPT_evaluator.period + 1) <= len(GPT_evaluator.call_indicator(data)) <= len(data)\n\n\ndef test_get_candles_data_api(GPT_evaluator):\n    for source in GPT_evaluator.SOURCES:\n        GPT_evaluator.source = source\n        if GPT_evaluator.source not in GPT_evaluator.get_unformated_sources():\n            assert isinstance(GPT_evaluator.get_candles_data_api(), types.FunctionType)\n\n\ndef test_parse_prediction_side(GPT_evaluator):\n    assert GPT_evaluator._parse_prediction_side(\"up 70%\") == -1\n    assert GPT_evaluator._parse_prediction_side(\"plop up 70%\") == -1\n    assert GPT_evaluator._parse_prediction_side(\" up with 70%\") == -1\n    assert GPT_evaluator._parse_prediction_side(\"Prediction: up with 70% confidence\") == -1\n\n    assert GPT_evaluator._parse_prediction_side(\"down 70%\") == 1\n    assert GPT_evaluator._parse_prediction_side(\"plop down 70%\") == 1\n    assert GPT_evaluator._parse_prediction_side(\" down with 70%\") == 1\n    assert GPT_evaluator._parse_prediction_side(\"Prediction: down with 70% confidence\") == 1\n\n\ndef test_parse_confidence(GPT_evaluator):\n    assert GPT_evaluator._parse_confidence(\"up 70%\") == 70\n    assert GPT_evaluator._parse_confidence(\"up 54.33%\") == 54.33\n    assert GPT_evaluator._parse_confidence(\"down 70% confidence blablabla\") == 70\n    assert GPT_evaluator._parse_confidence(\"Prediction: down 70%\") == 70\n    GPT_evaluator.min_confidence_threshold = 60\n    assert GPT_evaluator._parse_confidence(\"up 70%\") == 100\n    assert GPT_evaluator._parse_confidence(\"up 60%\") == 100\n    assert GPT_evaluator._parse_confidence(\"up 59%\") == 59\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/__init__.py",
    "content": "from .momentum import RSIMomentumEvaluator, ADXMomentumEvaluator, RSIWeightMomentumEvaluator, \\\n    BBMomentumEvaluator, MACDMomentumEvaluator, KlingerOscillatorMomentumEvaluator, \\\n    KlingerOscillatorReversalConfirmationMomentumEvaluator, EMAMomentumEvaluator"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/ADXMomentumEvaluator.json",
    "content": "{\n    \"period_length\": 14\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/BBMomentumEvaluator.json",
    "content": "{\n    \"period_length\": 20\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/EMAMomentumEvaluator.json",
    "content": "{\n    \"period_length\": 21,\n    \"price_threshold_percent\": 2\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/KlingerOscillatorMomentumEvaluator.json",
    "content": "{\n    \"ema_signal_period\": 13,\n    \"long_period\": 55,\n    \"short_period\": 35\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/KlingerOscillatorReversalConfirmationMomentumEvaluator.json",
    "content": "{\n    \"ema_signal_period\": 13,\n    \"long_period\": 55,\n    \"short_period\": 35\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/MACDMomentumEvaluator.json",
    "content": "{\n    \"long_period_length\": 26,\n    \"short_period_length\": 12,\n    \"signal_period_length\": 9\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/RSIMomentumEvaluator.json",
    "content": "{\n    \"long_threshold\": 30,\n    \"period_length\": 14,\n    \"short_threshold\": 70,\n    \"trend_change_identifier\": true\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/config/RSIWeightMomentumEvaluator.json",
    "content": "{\n  \"period\": 14,\n  \"slow_eval_count\": 16,\n  \"fast_eval_count\": 4,\n  \"RSI_to_weight\": [\n    {\n      \"slow_threshold\": 30,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 20,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 2\n              }\n          },\n          {\n            \"fast_threshold\" : 30,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 35,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 20,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 3\n              }\n          },\n          {\n            \"fast_threshold\" : 35,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 45,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 20,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 3\n              }\n          },\n          {\n            \"fast_threshold\" : 40,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 55,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 45,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 65,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 45,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          },\n          {\n            \"fast_threshold\" : 55,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 2\n              }\n          },\n          {\n            \"fast_threshold\" : 60,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 70,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 55,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 2\n              }\n          },\n          {\n            \"fast_threshold\" : 70,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 2\n              }\n          }\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"RSIMomentumEvaluator\", \"ADXMomentumEvaluator\", \"RSIWeightMomentumEvaluator\", \"BBMomentumEvaluator\",\n    \"MACDMomentumEvaluator\", \"KlingerOscillatorMomentumEvaluator\",\n    \"KlingerOscillatorReversalConfirmationMomentumEvaluator\", \"EMAMomentumEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/momentum.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport math\nimport numpy\nimport tulipy\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as enums\nimport octobot_commons.data_util as data_util\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.util as evaluators_util\nimport octobot_trading.api as trading_api\nimport tentacles.Evaluator.Util as EvaluatorUtil\n\n\nclass RSIMomentumEvaluator(evaluators.TAEvaluator):\n    PERIOD_LENGTH = \"period_length\"\n    TREND_CHANGE_IDENTIFIER = \"trend_change_identifier\"\n    LONG_THRESHOLD = \"long_threshold\"\n    SHORT_THRESHOLD = \"short_threshold\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.pertinence = 1\n        self.period_length = 14\n        self.short_threshold = 70\n        self.long_threshold = 30\n        self.is_trend_change_identifier = True\n        self.short_term_averages = [7, 5, 4, 3, 2, 1]\n        self.long_term_averages = [40, 30, 20, 15, 10]\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the evaluator, should define all the evaluator's user inputs\n        \"\"\"\n        default_config = self.get_default_config()\n        self.period_length = self.UI.user_input(\n            self.PERIOD_LENGTH, enums.UserInputTypes.INT, default_config[\"period_length\"],\n            inputs, min_val=0, title=\"RSI period length\"\n        )\n\n        self.is_trend_change_identifier = self.UI.user_input(\n            self.TREND_CHANGE_IDENTIFIER, enums.UserInputTypes.BOOLEAN,\n            default_config[\"trend_change_identifier\"], inputs,\n            title=\"Trend identifier: Identify RSI trend changes and evaluate the trend changes strength\",\n        )\n        self.short_threshold = self.UI.user_input(\n            self.SHORT_THRESHOLD, enums.UserInputTypes.FLOAT, default_config[\"short_threshold\"], inputs,\n            min_val=0,\n            title=\"Short threshold: RSI value from with to send a short (sell) signal. \"\n                  \"Evaluates as 1 when the current RSI value is equal or higher.\",\n            editor_options={\n                enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"trend_change_identifier\": False\n                }\n            }\n        )\n        self.long_threshold = self.UI.user_input(\n            self.LONG_THRESHOLD, enums.UserInputTypes.FLOAT, default_config[\"long_threshold\"], inputs,\n            min_val=0,\n            title=\"Long threshold: RSI value from with to send a long (buy) signal. \"\n                  \"Evaluates as -1 when the current RSI value is equal or lower.\",\n            editor_options={\n                enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"trend_change_identifier\": False\n                }\n            }\n        )\n\n    @classmethod\n    def get_default_config(\n        cls, period_length: typing.Optional[float] = None, trend_change_identifier: typing.Optional[bool] = None,\n        short_threshold: typing.Optional[float] = None, long_threshold: typing.Optional[float] = None\n    ):\n        return {\n            cls.PERIOD_LENGTH: period_length or 14,\n            cls.TREND_CHANGE_IDENTIFIER: True if trend_change_identifier is None else trend_change_identifier,\n            cls.SHORT_THRESHOLD: short_threshold or 70,\n            cls.LONG_THRESHOLD: long_threshold or 30,\n        }\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        updated_value = False\n        if candle_data is not None and len(candle_data) > self.period_length:\n            rsi_v = tulipy.rsi(candle_data, period=self.period_length)\n            if len(rsi_v) and not math.isnan(rsi_v[-1]):\n                if self.is_trend_change_identifier:\n                    long_trend = EvaluatorUtil.TrendAnalysis.get_trend(rsi_v, self.long_term_averages)\n                    short_trend = EvaluatorUtil.TrendAnalysis.get_trend(rsi_v, self.short_term_averages)\n\n                    # check if trend change\n                    if short_trend > 0 > long_trend:\n                        # trend changed to up\n                        self.set_eval_note(-short_trend)\n\n                    elif long_trend > 0 > short_trend:\n                        # trend changed to down\n                        self.set_eval_note(short_trend)\n\n                    # use RSI current value\n                    last_rsi_value = rsi_v[-1]\n                    if last_rsi_value > 50:\n                        self.set_eval_note(rsi_v[-1] / 200)\n                    else:\n                        self.set_eval_note((rsi_v[-1] - 100) / 200)\n                else:\n                    self.eval_note = 0\n                    if rsi_v[-1] >= self.short_threshold:\n                        self.eval_note = 1\n                    elif rsi_v[-1] <= self.long_threshold:\n                        self.eval_note = -1\n                updated_value = True\n        if not self.is_trend_change_identifier and not updated_value:\n            self.eval_note = 0\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not symbol dependant else False\n        \"\"\"\n        return False\n\n    @classmethod\n    def get_is_time_frame_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the evaluator is not time_frame dependant else False\n        \"\"\"\n        return False\n\n\n# double RSI analysis\nclass RSIWeightMomentumEvaluator(evaluators.TAEvaluator):\n    PERIOD = \"period\"\n    SLOW_EVAL_COUNT = \"slow_eval_count\"\n    FAST_EVAL_COUNT = \"fast_eval_count\"\n    RSI_TO_WEIGHTS = \"RSI_to_weight\"\n    SLOW_THRESHOLD = \"slow_threshold\"\n    FAST_THRESHOLD = \"fast_threshold\"\n    FAST_THRESHOLDS = \"fast_thresholds\"\n    WEIGHTS = \"weights\"\n    PRICE = \"price\"\n    VOLUME = \"volume\"\n\n    @staticmethod\n    def get_eval_type():\n        return typing.Dict[str, int]\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.period_length = 14\n        self.slow_eval_count = 16\n        self.fast_eval_count = 4\n        self.weights = []\n\n    def _init_fast_threshold(self, inputs, indexes, fast_threshold, price_weight, volume_weight):\n        self.UI.user_input(self.WEIGHTS, enums.UserInputTypes.OBJECT, None, inputs,\n                           parent_input_name=self.FAST_THRESHOLDS,\n                           title=\"Price and volume weights of this interpretation.\", array_indexes=indexes)\n        return {\n            self.FAST_THRESHOLD: self.UI.user_input(self.FAST_THRESHOLD, enums.UserInputTypes.INT, fast_threshold,\n                                                    inputs, min_val=0, parent_input_name=self.FAST_THRESHOLDS,\n                                                    title=\"Fast RSI threshold under which this interpretation will \"\n                                                          \"be triggered.\", array_indexes=indexes),\n            self.WEIGHTS: {\n                self.PRICE: self.UI.user_input(self.PRICE, enums.UserInputTypes.OPTIONS, price_weight,\n                                               inputs, options=[1, 2, 3], parent_input_name=self.WEIGHTS,\n                                               editor_options={\"enum_titles\": [\"Light\", \"Average\", \"Heavy\"]},\n                                               title=\"Price weight.\", array_indexes=indexes),\n                self.VOLUME: self.UI.user_input(self.VOLUME, enums.UserInputTypes.OPTIONS, volume_weight,\n                                                inputs, options=[1, 2, 3], parent_input_name=self.WEIGHTS,\n                                                editor_options={\"enum_titles\": [\"Light\", \"Average\", \"Heavy\"]},\n                                                title=\"Volume weight.\", array_indexes=indexes),\n            }\n        }\n\n    def _init_RSI_to_weight(self, inputs, slow_threshold, fast_thresholds):\n        self.UI.user_input(self.FAST_THRESHOLDS, enums.UserInputTypes.OBJECT_ARRAY, fast_thresholds, inputs,\n                           item_title=\"Fast RSI interpretation\",\n                           other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n                           parent_input_name=self.RSI_TO_WEIGHTS,\n                           title=\"Interpretations on this slow threshold trigger case.\"),\n        return {\n            self.SLOW_THRESHOLD: self.UI.user_input(self.SLOW_THRESHOLD, enums.UserInputTypes.INT, slow_threshold,\n                                                    inputs,\n                                                    min_val=0, parent_input_name=self.RSI_TO_WEIGHTS,\n                                                    title=\"Slow RSI threshold under which this interpretation will \"\n                                                          \"be triggered.\", array_indexes=[0]),\n            self.FAST_THRESHOLDS: [\n                self._init_fast_threshold(inputs, [0, index], *fast_threshold)\n                for index, fast_threshold in enumerate(fast_thresholds)\n            ],\n        }\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.period_length = self.UI.user_input(\"period\", enums.UserInputTypes.INT, self.period_length,\n                                                inputs, min_val=1,\n                                                title=\"Period: RSI period length.\")\n        self.slow_eval_count = self.UI.user_input(\"slow_eval_count\", enums.UserInputTypes.INT, self.slow_eval_count,\n                                                  inputs, min_val=1,\n                                                  title=\"Number of recent RSI values to consider to get the current slow \"\n                                                        \"moving market sentiment.\")\n        self.fast_eval_count = self.UI.user_input(\"fast_eval_count\", enums.UserInputTypes.INT, self.fast_eval_count,\n                                                  inputs, min_val=1,\n                                                  title=\"Number of recent RSI values to consider to get the current fast \"\n                                                        \"moving market sentiment.\")\n        weights = []\n        self.weights = sorted(\n            self.UI.user_input(self.RSI_TO_WEIGHTS, enums.UserInputTypes.OBJECT_ARRAY, weights, inputs,\n                               item_title=\"Slow RSI interpretation\",\n                               other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n                               title=\"RSI values and interpretations.\"),\n            key=lambda a: a[self.SLOW_THRESHOLD]\n        )\n        # init one user input to generate user input schema and default values\n        weights.append(self._init_RSI_to_weight(inputs, 30, [[20, 2, 2]]))\n\n        for i, fast_threshold in enumerate(self.weights):\n            fast_threshold[self.FAST_THRESHOLDS] = sorted(fast_threshold[self.FAST_THRESHOLDS],\n                                                          key=lambda a: a[self.FAST_THRESHOLD])\n\n    def _get_rsi_averages(self, symbol_candles, time_frame, include_in_construction):\n        # compute the slow and fast RSI average\n        candle_data = trading_api.get_symbol_close_candles(symbol_candles, time_frame,\n                                                           include_in_construction=include_in_construction)\n        if len(candle_data) > self.period_length:\n            rsi_v = tulipy.rsi(candle_data, period=self.period_length)\n            rsi_v = data_util.drop_nan(rsi_v)\n            if len(rsi_v):\n                slow_average = numpy.mean(rsi_v[-self.slow_eval_count:])\n                fast_average = numpy.mean(rsi_v[-self.fast_eval_count:])\n                return slow_average, fast_average, rsi_v\n        return None, None, None\n\n    @staticmethod\n    def _check_inferior(bound, val1, val2):\n        return val1 < bound and val2 < bound\n\n    def _analyse_dip_weight(self, slow_rsi, fast_rsi, current_rsi):\n        # returns price weight, volume weight\n        try:\n            for slow_rsi_weight in self.weights:\n                if slow_rsi < slow_rsi_weight[self.SLOW_THRESHOLD]:\n                    for fast_rsi_weight in slow_rsi_weight[self.FAST_THRESHOLDS]:\n                        if self._check_inferior(fast_rsi_weight[self.FAST_THRESHOLD], fast_rsi, current_rsi):\n                            return fast_rsi_weight[self.WEIGHTS][self.PRICE], \\\n                                   fast_rsi_weight[self.WEIGHTS][self.VOLUME]\n                    # exit loop since the target RSI has been found\n                    break\n        except KeyError as e:\n            self.logger.error(f\"Error when reading from config file: missing {e}\")\n        return None, None\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        try:\n            symbol_candles = self.get_exchange_symbol_data(exchange, exchange_id, symbol)\n            # compute the slow and fast RSI average\n            slow_rsi, fast_rsi, rsi_v = self._get_rsi_averages(symbol_candles, time_frame,\n                                                               include_in_construction=inc_in_construction_data)\n            current_candle_time = trading_api.get_symbol_time_candles(symbol_candles, time_frame,\n                                                                      include_in_construction=inc_in_construction_data)[\n                -1]\n            await self.evaluate(cryptocurrency, symbol, time_frame, slow_rsi,\n                                fast_rsi, rsi_v, current_candle_time, candle)\n        except IndexError:\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, slow_rsi,\n                       fast_rsi, rsi_v, current_candle_time, candle):\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if slow_rsi is not None and fast_rsi is not None and rsi_v is not None:\n            last_rsi_values_to_consider = 5\n            analysed_rsi = rsi_v[-last_rsi_values_to_consider:]\n            peak_reached = EvaluatorUtil.TrendAnalysis.min_has_just_been_reached(analysed_rsi, acceptance_window=0.95,\n                                                                                 delay=2)\n            if peak_reached:\n                price_weight, volume_weight = self._analyse_dip_weight(slow_rsi, fast_rsi, rsi_v[-1])\n                if price_weight is not None and volume_weight is not None:\n                    self.eval_note = {\n                        \"price_weight\": price_weight,\n                        \"volume_weight\": volume_weight,\n                        \"current_candle_time\": current_candle_time\n                    }\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n\n# bollinger_bands\nclass BBMomentumEvaluator(evaluators.TAEvaluator):\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.period_length = 20\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.period_length = self.UI.user_input(\"period_length\", enums.UserInputTypes.INT, self.period_length,\n                                                inputs, min_val=1,\n                                                title=\"Period: Bollinger bands period length.\")\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           self.period_length,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(candle_data) >= self.period_length:\n            # compute bollinger bands\n            lower_band, middle_band, upper_band = tulipy.bbands(candle_data, self.period_length, 2)\n\n            # if close to lower band => low value => bad,\n            # therefore if close to middle, value is keeping up => good\n            # finally if up the middle one or even close to the upper band => very good\n\n            current_value = candle_data[-1]\n            current_up = upper_band[-1]\n            current_middle = middle_band[-1]\n            current_low = lower_band[-1]\n            delta_up = current_up - current_middle\n            delta_low = current_middle - current_low\n\n            # its exactly on all bands\n            if current_up == current_low:\n                self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n\n            # exactly on the middle\n            elif current_value == current_middle:\n                self.eval_note = 0\n\n            # up the upper band\n            elif current_value > current_up:\n                self.eval_note = 1\n\n            # down the lower band\n            elif current_value < current_low:\n                self.eval_note = -1\n\n            # regular values case: use parabolic factor all the time\n            else:\n\n                # up the middle band\n                if current_middle < current_value:\n                    self.eval_note = math.pow((current_value - current_middle) / delta_up, 2)\n\n                # down the middle band\n                elif current_middle > current_value:\n                    self.eval_note = -1 * math.pow((current_middle - current_value) / delta_low, 2)\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n\n# EMA\nclass EMAMomentumEvaluator(evaluators.TAEvaluator):\n    PERIOD_LENGTH = \"period_length\"\n    PRICE_THRESHOLD_PERCENT = \"price_threshold_percent\"\n    REVERSE_SIGNAL = \"reverse_signal\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.period_length = 21\n        self.price_threshold_percent = 2\n        self.price_threshold_multiplier = self.price_threshold_percent / 100\n        self.reverse_signal = False\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        default_config = self.get_default_config()\n        self.period_length = self.UI.user_input(\n            self.PERIOD_LENGTH, enums.UserInputTypes.INT, default_config[\"period_length\"], inputs,\n            min_val=1, title=\"Period: Moving Average period length.\"\n        )\n        self.price_threshold_percent = self.UI.user_input(\n            self.PRICE_THRESHOLD_PERCENT, enums.UserInputTypes.FLOAT,\n            default_config[\"price_threshold_percent\"], inputs,\n            min_val=0,\n            title=\"Price threshold: Percent difference between the current price and current EMA value from \"\n                  \"which to trigger a long or short signal. \"\n                  \"Example with EMA value=200, Price threshold=5: a short signal will fire when price is above or \"\n                  \"equal to 210 and a long signal will when price is bellow or equal to 190\",\n        )\n        self.reverse_signal = self.UI.user_input(\n            self.REVERSE_SIGNAL, enums.UserInputTypes.BOOLEAN, default_config[\"reverse_signal\"], inputs,\n            title=\"Reverse signal: when enabled, emits a short signal when the current price is bellow the EMA \"\n                  \"value and long signal when the current price is above the EMA value.\",\n        )\n        self.price_threshold_multiplier = self.price_threshold_percent / 100\n\n    @classmethod\n    def get_default_config(\n        cls,\n        period_length: typing.Optional[int] = None, price_threshold_percent: typing.Optional[float] = None,\n        reverse_signal: typing.Optional[bool] = False,\n    ) -> dict:\n        return {\n            cls.PERIOD_LENGTH: period_length or 21,\n            cls.PRICE_THRESHOLD_PERCENT: 2 if price_threshold_percent is None else price_threshold_percent,\n            cls.REVERSE_SIGNAL: reverse_signal or False,\n        }\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           self.period_length,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        self.eval_note = 0\n        if len(candle_data) >= self.period_length:\n            # compute ema\n            ema_values = tulipy.ema(candle_data, self.period_length)\n            is_price_above_ema_threshold = candle_data[-1] >= (ema_values[-1] * (1 + self.price_threshold_multiplier))\n            is_price_bellow_ema_threshold = candle_data[-1] <= (ema_values[-1] * (1 - self.price_threshold_multiplier))\n            if is_price_above_ema_threshold:\n                self.eval_note = 1\n            elif is_price_bellow_ema_threshold:\n                self.eval_note = -1\n            if self.reverse_signal:\n                self.eval_note = -1 * self.eval_note\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n\n# ADX --> trend_strength\nclass ADXMomentumEvaluator(evaluators.TAEvaluator):\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.period_length = 14\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.period_length = self.UI.user_input(\"period_length\", enums.UserInputTypes.INT, self.period_length,\n                                                inputs, min_val=1,\n                                                title=\"Period: ADX period length.\")\n\n    def _get_minimal_data(self):\n        # 26 minimal_data length required for 14 period_length\n        return self.period_length + 12\n\n    # implementation according to: https://www.investopedia.com/articles/technical/02/041002.asp => length = 14 and\n    # exponential moving average = 20 in a uptrend market\n    # idea: adx > 30 => strong trend, < 20 => trend change to come\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        symbol_candles = self.get_exchange_symbol_data(exchange, exchange_id, symbol)\n        close_candles = trading_api.get_symbol_close_candles(symbol_candles, time_frame,\n                                                             include_in_construction=inc_in_construction_data)\n        if len(close_candles) > self._get_minimal_data():\n            high_candles = trading_api.get_symbol_high_candles(symbol_candles, time_frame,\n                                                               include_in_construction=inc_in_construction_data)\n            low_candles = trading_api.get_symbol_low_candles(symbol_candles, time_frame,\n                                                             include_in_construction=inc_in_construction_data)\n            await self.evaluate(cryptocurrency, symbol, time_frame, close_candles, high_candles, low_candles, candle)\n        else:\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n            await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                            eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                    time_frame=time_frame))\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, close_candles, high_candles, low_candles, candle):\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(close_candles) >= self._get_minimal_data():\n            min_adx = 7.5\n            max_adx = 45\n            neutral_adx = 25\n            adx = tulipy.adx(high_candles, low_candles, close_candles, self.period_length)\n            instant_ema = data_util.drop_nan(tulipy.ema(close_candles, 2))\n            slow_ema = data_util.drop_nan(tulipy.ema(close_candles, 20))\n            adx = data_util.drop_nan(adx)\n\n            if len(adx):\n                current_adx = adx[-1]\n                current_slows_ema = slow_ema[-1]\n                current_instant_ema = instant_ema[-1]\n\n                multiplier = -1 if current_instant_ema < current_slows_ema else 1\n\n                # strong adx => strong trend\n                if current_adx > neutral_adx:\n                    # if max adx already reached => when ADX forms a top and begins to turn down, you should look for a\n                    # retracement that causes the price to move toward its 20-day exponential moving average (EMA).\n                    adx_last_values = adx[-15:]\n                    adx_last_value = adx_last_values[-1]\n\n                    local_max_adx = adx_last_values.max()\n                    # max already reached => trend will slow down\n                    if adx_last_value < local_max_adx:\n\n                        self.eval_note = multiplier * (current_adx - neutral_adx) / (local_max_adx - neutral_adx)\n\n                    # max not reached => trend will continue, return chances to be max now\n                    else:\n                        crossing_indexes = EvaluatorUtil.TrendAnalysis.get_threshold_change_indexes(adx, neutral_adx)\n                        chances_to_be_max = \\\n                            EvaluatorUtil.TrendAnalysis.get_estimation_of_move_state_relatively_to_previous_moves_length(\n                                crossing_indexes, adx) if len(crossing_indexes) > 2 else 0.75\n                        proximity_to_max = min(1, current_adx / max_adx)\n                        self.eval_note = multiplier * proximity_to_max * chances_to_be_max\n\n                # weak adx => change to come\n                else:\n                    self.eval_note = multiplier * min(1, ((neutral_adx - current_adx) / (neutral_adx - min_adx)))\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n\nclass MACDMomentumEvaluator(evaluators.TAEvaluator):\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.previous_note = None\n        self.long_period_length = 26\n        self.short_period_length = 12\n        self.signal_period_length = 9\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.short_period_length = self.UI.user_input(\n            \"short_period_length\", enums.UserInputTypes.INT, self.short_period_length, inputs,\n            min_val=1, title=\"MACD fast period length.\"\n        )\n        self.long_period_length = self.UI.user_input(\n            \"long_period_length\", enums.UserInputTypes.INT, self.long_period_length, inputs,\n            min_val=1, title=\"MACD slow period length.\"\n        )\n        self.signal_period_length = self.UI.user_input(\n            \"signal_period_length\", enums.UserInputTypes.INT, self.signal_period_length, inputs,\n            min_val=1, title=\"MACD signal period.\"\n        )\n\n    def _analyse_pattern(self, pattern, macd_hist, zero_crossing_indexes, price_weight,\n                         pattern_move_time, sign_multiplier):\n        # add pattern's strength\n        weight = price_weight * EvaluatorUtil.PatternAnalyser.get_pattern_strength(pattern)\n\n        average_pattern_period = 0.7\n        if len(zero_crossing_indexes) > 1:\n            # compute chances to be after average pattern period\n            patterns = [EvaluatorUtil.PatternAnalyser.get_pattern(\n                macd_hist[zero_crossing_indexes[i]:zero_crossing_indexes[i + 1]])\n                for i in range(len(zero_crossing_indexes) - 1)\n            ]\n            if 0 != zero_crossing_indexes[0]:\n                patterns.append(EvaluatorUtil.PatternAnalyser.get_pattern(macd_hist[0:zero_crossing_indexes[0]]))\n            if len(macd_hist) - 1 != zero_crossing_indexes[-1]:\n                patterns.append(EvaluatorUtil.PatternAnalyser.get_pattern(macd_hist[zero_crossing_indexes[-1]:]))\n            double_patterns_count = patterns.count(\"W\") + patterns.count(\"M\")\n\n            average_pattern_period = EvaluatorUtil.TrendAnalysis. \\\n                get_estimation_of_move_state_relatively_to_previous_moves_length(\n                zero_crossing_indexes,\n                macd_hist,\n                pattern_move_time,\n                double_patterns_count)\n\n        # if we have few data but wave is growing => set higher value\n        if len(zero_crossing_indexes) <= 1 and price_weight == 1:\n            if self.previous_note is not None:\n                average_pattern_period = 0.95\n            self.previous_note = sign_multiplier * weight * average_pattern_period\n        else:\n            self.previous_note = None\n\n        self.eval_note = sign_multiplier * weight * average_pattern_period\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(candle_data) > self.long_period_length:\n            macd, macd_signal, macd_hist = tulipy.macd(candle_data, self.short_period_length,\n                                                       self.long_period_length, self.signal_period_length)\n\n            # on macd hist => M pattern: bearish movement, W pattern: bullish movement\n            #                 max on hist: optimal sell or buy\n            macd_hist = data_util.drop_nan(macd_hist)\n            zero_crossing_indexes = EvaluatorUtil.TrendAnalysis.get_threshold_change_indexes(macd_hist, 0)\n            last_index = len(macd_hist) - 1\n            pattern, start_index, end_index = EvaluatorUtil.PatternAnalyser.find_pattern(macd_hist,\n                                                                                         zero_crossing_indexes,\n                                                                                         last_index)\n\n            if pattern != EvaluatorUtil.PatternAnalyser.UNKNOWN_PATTERN:\n\n                # set sign (-1 buy or 1 sell)\n                sign_multiplier = -1 if pattern == \"W\" or pattern == \"V\" else 1\n\n                # set pattern time frame => W and M are on 2 time frames, others 1\n                pattern_move_time = 2 if (pattern == \"W\" or pattern == \"M\") and end_index == last_index else 1\n\n                # set weight according to the max value of the pattern and the current value\n                current_pattern_start = start_index\n                price_weight = macd_hist[-1] / macd_hist[current_pattern_start:].max() if sign_multiplier == 1 \\\n                    else macd_hist[-1] / macd_hist[current_pattern_start:].min()\n\n                if not math.isnan(price_weight):\n                    self._analyse_pattern(pattern, macd_hist, zero_crossing_indexes, price_weight,\n                                          pattern_move_time, sign_multiplier)\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n\nclass KlingerOscillatorMomentumEvaluator(evaluators.TAEvaluator):\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.short_period = 35  # standard with klinger\n        self.long_period = 55  # standard with klinger\n        self.ema_signal_period = 13  # standard ema signal for klinger\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.short_period = self.UI.user_input(\"short_period\", enums.UserInputTypes.INT, self.short_period,\n                                               inputs, min_val=1,\n                                               title=\"Short period: length of the short klinger period (standard is 35).\")\n        self.long_period = self.UI.user_input(\"long_period\", enums.UserInputTypes.INT, self.long_period,\n                                              inputs, min_val=1,\n                                              title=\"Long period: length of the long klinger period (standard is 55).\")\n        self.ema_signal_period = self.UI.user_input(\"ema_signal_period\", enums.UserInputTypes.INT,\n                                                    self.ema_signal_period,\n                                                    inputs, min_val=1,\n                                                    title=\"Long period: length of the exponential moving average used \"\n                                                          \"to apply on the klinger results (standard is 13).\")\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        symbol_candles = self.get_exchange_symbol_data(exchange, exchange_id, symbol)\n        high_candles = trading_api.get_symbol_high_candles(symbol_candles, time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        if len(high_candles) >= self.short_period:\n            low_candles = trading_api.get_symbol_low_candles(symbol_candles, time_frame,\n                                                             include_in_construction=inc_in_construction_data)\n            close_candles = trading_api.get_symbol_close_candles(symbol_candles, time_frame,\n                                                                 include_in_construction=inc_in_construction_data)\n            volume_candles = trading_api.get_symbol_volume_candles(symbol_candles, time_frame,\n                                                                   include_in_construction=inc_in_construction_data)\n            await self.evaluate(cryptocurrency, symbol, time_frame, high_candles, low_candles,\n                                close_candles, volume_candles, candle)\n        else:\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n            await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                            eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                    time_frame=time_frame))\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, high_candles, low_candles,\n                       close_candles, volume_candles, candle):\n        eval_proposition = commons_constants.START_PENDING_EVAL_NOTE\n        kvo = tulipy.kvo(high_candles,\n                         low_candles,\n                         close_candles,\n                         volume_candles,\n                         self.short_period,\n                         self.long_period)\n        kvo = data_util.drop_nan(kvo)\n        if len(kvo) >= self.ema_signal_period:\n            kvo_ema = tulipy.ema(kvo, self.ema_signal_period)\n\n            ema_difference = kvo - kvo_ema\n\n            if len(ema_difference) > 1:\n                zero_crossing_indexes = EvaluatorUtil.TrendAnalysis.get_threshold_change_indexes(ema_difference, 0)\n\n                current_difference = ema_difference[-1]\n                significant_move_threshold = numpy.std(ema_difference)\n\n                factor = 0.2\n\n                if EvaluatorUtil.TrendAnalysis.peak_has_been_reached_already(\n                        ema_difference[zero_crossing_indexes[-1]:]):\n                    if abs(current_difference) > significant_move_threshold:\n                        factor = 1\n                    else:\n                        factor = 0.5\n\n                eval_proposition = current_difference * factor / significant_move_threshold\n\n                if abs(eval_proposition) > 1:\n                    eval_proposition = 1 if eval_proposition > 0 else -1\n        self.eval_note = eval_proposition\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n\nclass KlingerOscillatorReversalConfirmationMomentumEvaluator(evaluators.TAEvaluator):\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.short_period = 35  # standard with klinger\n        self.long_period = 55  # standard with klinger\n        self.ema_signal_period = 13  # standard ema signal for klinger\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.short_period = self.UI.user_input(\"short_period\", enums.UserInputTypes.INT, self.short_period,\n                                               inputs, min_val=1,\n                                               title=\"Short period: length of the short klinger period (standard is 35).\")\n        self.long_period = self.UI.user_input(\"long_period\", enums.UserInputTypes.INT, self.long_period,\n                                              inputs, min_val=1,\n                                              title=\"Long period: length of the long klinger period (standard is 55).\")\n        self.ema_signal_period = self.UI.user_input(\"ema_signal_period\", enums.UserInputTypes.INT,\n                                                    self.ema_signal_period,\n                                                    inputs, min_val=1,\n                                                    title=\"Long period: length of the exponential moving average used \"\n                                                          \"to apply on the klinger results (standard is 13).\")\n\n    @staticmethod\n    def get_eval_type():\n        return bool\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        symbol_candles = self.get_exchange_symbol_data(exchange, exchange_id, symbol)\n        high_candles = trading_api.get_symbol_high_candles(symbol_candles, time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        if len(high_candles) >= self.short_period:\n            low_candles = trading_api.get_symbol_low_candles(symbol_candles, time_frame,\n                                                             include_in_construction=inc_in_construction_data)\n            close_candles = trading_api.get_symbol_close_candles(symbol_candles, time_frame,\n                                                                 include_in_construction=inc_in_construction_data)\n            volume_candles = trading_api.get_symbol_volume_candles(symbol_candles, time_frame,\n                                                                   include_in_construction=inc_in_construction_data)\n            await self.evaluate(cryptocurrency, symbol, time_frame, high_candles, low_candles,\n                                close_candles, volume_candles, candle)\n        else:\n            self.eval_note = False\n            await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                            eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                    time_frame=time_frame))\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, high_candles, low_candles,\n                       close_candles, volume_candles, candle):\n        if len(high_candles) >= self.short_period:\n            kvo = tulipy.kvo(high_candles,\n                             low_candles,\n                             close_candles,\n                             volume_candles,\n                             self.short_period,\n                             self.long_period)\n            kvo = data_util.drop_nan(kvo)\n            if len(kvo) >= self.ema_signal_period:\n\n                kvo_ema = tulipy.ema(kvo, self.ema_signal_period)\n                ema_difference = kvo - kvo_ema\n\n                if len(ema_difference) > 1:\n                    zero_crossing_indexes = EvaluatorUtil.TrendAnalysis.get_threshold_change_indexes(ema_difference, 0)\n                    max_elements = 7\n                    to_consider_kvo = min(max_elements, len(ema_difference) - zero_crossing_indexes[-1])\n                    self.eval_note = EvaluatorUtil.TrendAnalysis.min_has_just_been_reached(\n                        ema_difference[-to_consider_kvo:],\n                        acceptance_window=0.9, delay=1)\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/ADXMomentumEvaluator.md",
    "content": "Uses the [Average Directional Index](https://www.investopedia.com/terms/a/adx.asp)  \nto find reversals. The default implementation is according to \n[Investopedia's ADX: The Trend Strength Indicator](https://www.investopedia.com/articles/technical/02/041002.asp).\n\nEvaluates -1 to 1 according to the current price using the \n[Exponential Moving Average](https://www.investopedia.com/terms/e/ema.asp) with a length of 20 coupled with \nthe [ADX](https://www.investopedia.com/terms/a/adx.asp).\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/BBMomentumEvaluator.md",
    "content": "Uses the [Bollinger bands](https://www.investopedia.com/terms/b/bollingerbands.asp)  to evaluate a value from -1 to 1 according to the current price \ndistance from to the [Bollinger bands](https://www.investopedia.com/terms/b/bollingerbands.asp) values.\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/EMAMomentumEvaluator.md",
    "content": "Uses  [exponential moving averages](https://www.investopedia.com/terms/m/movingaverage.asp) to find signal when the current price exceeds the average value.\n\nEvaluates -1 or 1 when the current price is far enough from the EMA."
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/KlingerOscillatorMomentumEvaluator.md",
    "content": "Uses [Klinger Oscillator](https://www.investopedia.com/terms/k/klingeroscillator.asp) to find reversals.\n\nEvaluates -1 to 1 using [Klinger](https://www.investopedia.com/terms/k/klingeroscillator.asp) reversal estimation"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/KlingerOscillatorReversalConfirmationMomentumEvaluator.md",
    "content": "Uses [Klinger Oscillator](https://www.investopedia.com/terms/k/klingeroscillator.asp) to find reversals.\n\nReturns True on reversal confirmation."
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/MACDMomentumEvaluator.md",
    "content": "Uses the [Moving Average Convergence Divergence](https://www.investopedia.com/terms/m/macd.asp) to find reversals.\n\nThis evaluator will try to find patterns in the [MACD](https://www.investopedia.com/terms/m/macd.asp) histogram and returns -1 to 1 according to the price and identified pattern strength.\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/RSIMomentumEvaluator.md",
    "content": "Uses the [Relative Strength Index](https://www.investopedia.com/terms/r/rsi.asp) to find trend reversals. \n\nWhen found, evaluates -1 to 1 according to the strength of the [RSI](https://www.investopedia.com/terms/r/rsi.asp)."
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/resources/RSIWeightMomentumEvaluator.md",
    "content": "Uses the [Relative Strength Index](https://www.investopedia.com/terms/r/rsi.asp) to find dips and give them weight according to the trend\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/tests/test_adx_momentum_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport pytest_asyncio\n\nimport tests.functional_tests.evaluators_tests.abstract_TA_test as abstract_TA_test\nimport tentacles.Evaluator.TA as TA\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def evaluator_tester():\n    evaluator_tester_instance = TestADXTAEvaluator()\n    evaluator_tester_instance.TA_evaluator_class = TA.ADXMomentumEvaluator\n    return evaluator_tester_instance\n\n\nclass TestADXTAEvaluator(abstract_TA_test.AbstractTATest):\n\n    @staticmethod\n    async def test_stress_test(evaluator_tester):\n        await evaluator_tester.run_stress_test_without_exceptions(0.7)\n\n    @staticmethod\n    async def test_reactions_to_dump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_dump(0.2, 0.35, -0.2, -0.1, 0)\n\n    @staticmethod\n    async def test_reactions_to_pump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_pump(0, 0.1, 0.45, 0.7, 0.6, 0.65, 0.75)\n\n    @staticmethod\n    async def test_reaction_to_rise_after_over_sold(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_rise_after_over_sold(0.8, -0.1, -0.5, -0.52, 0.8)\n\n    @staticmethod\n    async def test_reaction_to_over_bought_then_dip(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_over_bought_then_dip(0.1, 0.1, 0.3, 0.4, -0.4, 0.2)\n\n    @staticmethod\n    async def test_reaction_to_flat_trend(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_flat_trend(\n            # eval_start_move_ending_up_in_a_rise,\n            0.4,\n            # eval_reaches_flat_trend, eval_first_micro_up_p1, eval_first_micro_up_p2,\n            0.1, 0.4, 0.45,\n            # eval_micro_down1, eval_micro_up1, eval_micro_down2, eval_micro_up2,\n            1, 0.6, 0.1, 0.4,\n            # eval_micro_down3, eval_back_normal3, eval_micro_down4, eval_back_normal4,\n            -0.4, 0.5, -0.7, 0.8,\n            # eval_micro_down5, eval_back_up5, eval_micro_up6, eval_back_down6,\n            -0.1, -0.5, 0.25, 0.35,\n            # eval_back_normal6, eval_micro_down7, eval_back_up7, eval_micro_down8,\n            0.3, -0.5, -0.6, -0.45,\n            # eval_back_up8, eval_micro_down9, eval_back_up9\n            -0.35, -0.1, 0.1)\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/tests/test_bollinger_bands_momentum_TA_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport pytest_asyncio\n\n\nimport tests.functional_tests.evaluators_tests.abstract_TA_test as abstract_TA_test\nimport tentacles.Evaluator.TA as TA\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def evaluator_tester():\n    evaluator_tester_instance = TestBollingerBandsMomentumeEvaluator()\n    evaluator_tester_instance.TA_evaluator_class = TA.BBMomentumEvaluator\n    return evaluator_tester_instance\n\n\nclass TestBollingerBandsMomentumeEvaluator(abstract_TA_test.AbstractTATest):\n\n    @staticmethod\n    async def test_stress_test(evaluator_tester):\n        await evaluator_tester.run_stress_test_without_exceptions()\n\n    @staticmethod\n    async def test_reactions_to_dump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_dump(0.7, 0.2, -1, -1, -1)\n\n    @staticmethod\n    async def test_reactions_to_pump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_pump(0.4, 0.5, 1, 1, 1, 1, 0.1)\n\n    @staticmethod\n    async def test_reaction_to_rise_after_over_sold(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_rise_after_over_sold(-0.1, -0.99, -0.99, -0.5, 1)\n\n    @staticmethod\n    async def test_reaction_to_over_bought_then_dip(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_over_bought_then_dip(0, 1, 1, 0.95, -0.3, -0.1)\n\n    @staticmethod\n    async def test_reaction_to_flat_trend(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_flat_trend(\n            # eval_start_move_ending_up_in_a_rise,\n            1,\n            # eval_reaches_flat_trend, eval_first_micro_up_p1, eval_first_micro_up_p2,\n            1, 0.8, 0.4,\n            # eval_micro_down1, eval_micro_up1, eval_micro_down2, eval_micro_up2,\n            0.1, 1, -0.3, 0.1,\n            # eval_micro_down3, eval_back_normal3, eval_micro_down4, eval_back_normal4,\n            -0.6, 0.5, 0, 0.5,\n            # eval_micro_down5, eval_back_up5, eval_micro_up6, eval_back_down6,\n            -1, -0.15, 1, 0.1,\n            # eval_back_normal6, eval_micro_down7, eval_back_up7, eval_micro_down8,\n            0.4, -0.1, 0, -1,\n            # eval_back_up8, eval_micro_down9, eval_back_up9\n            -0.05, -1, 0.5)\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/tests/test_klinger_TA_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport pytest_asyncio\n\n\nimport tests.functional_tests.evaluators_tests.abstract_TA_test as abstract_TA_test\nimport tentacles.Evaluator.TA as TA\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def evaluator_tester():\n    evaluator_tester_instance = TestKlingerEvaluator()\n    evaluator_tester_instance.TA_evaluator_class = TA.KlingerOscillatorMomentumEvaluator\n    return evaluator_tester_instance\n\n\nclass TestKlingerEvaluator(abstract_TA_test.AbstractTATest):\n\n    @staticmethod\n    async def test_stress_test(evaluator_tester):\n        await evaluator_tester.run_stress_test_without_exceptions(0.7, False, skip_long_time_frames=True)\n\n    @staticmethod\n    async def test_reactions_to_dump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_dump(0, 0, -0.2, -0.4, -0.55)\n\n    @staticmethod\n    async def test_reactions_to_pump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_pump(-0.1, -0.1, 0, 0.1, 0.2,\n                                                          0, -0.5)\n\n    @staticmethod\n    async def test_reaction_to_rise_after_over_sold(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_rise_after_over_sold(-0.2, -0.6, -1, -1, 0.1)\n\n    @staticmethod\n    async def test_reaction_to_over_bought_then_dip(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_over_bought_then_dip(-1, 0, 0.5, 0.5, -0.8, -1)\n\n    @staticmethod\n    async def test_reaction_to_flat_trend(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_flat_trend(\n            # eval_start_move_ending_up_in_a_rise,\n            0.9,\n            # eval_reaches_flat_trend, eval_first_micro_up_p1, eval_first_micro_up_p2,\n            0.7, 0.55, 0.3,\n            # eval_micro_down1, eval_micro_up1, eval_micro_down2, eval_micro_up2,\n            -0.3, -0.25, -0.4, -0.1,\n            # eval_micro_down3, eval_back_normal3, eval_micro_down4, eval_back_normal4,\n            0, -0.1, 0.1, 0.1,\n            # eval_micro_down5, eval_back_up5, eval_micro_up6, eval_back_down6,\n            -0.1, -0.1, 0.1, 0.25,\n            # eval_back_normal6, eval_micro_down7, eval_back_up7, eval_micro_down8,\n            0, 0.1, -0.2, 0,\n            # eval_back_up8, eval_micro_down9, eval_back_up9\n            0, 0, 0.1)\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/tests/test_macd_TA_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport pytest_asyncio\n\n\nimport tests.functional_tests.evaluators_tests.abstract_TA_test as abstract_TA_test\nimport tentacles.Evaluator.TA as TA\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def evaluator_tester():\n    evaluator_tester_instance = TestMACDEvaluator()\n    evaluator_tester_instance.TA_evaluator_class = TA.MACDMomentumEvaluator\n    return evaluator_tester_instance\n\n\nclass TestMACDEvaluator(abstract_TA_test.AbstractTATest):\n\n    @staticmethod\n    async def test_stress_test(evaluator_tester):\n        await evaluator_tester.run_stress_test_without_exceptions(0.6)\n\n    @staticmethod\n    async def test_reactions_to_dump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_dump(0.3, 0.25, -0.15, -0.3, -0.5)\n\n    @staticmethod\n    async def test_reactions_to_pump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_pump(0.3, 0.4, 0.75, 0.75, 0.75, 0.75, 0.2)\n\n    @staticmethod\n    async def test_reaction_to_rise_after_over_sold(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_rise_after_over_sold(0, -0.5, -0.65, -0.4, -0.08)\n\n    @staticmethod\n    async def test_reaction_to_over_bought_then_dip(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_over_bought_then_dip(-0.6, 0.1, 0.6, 0.7, -0.35, -0.65)\n\n    @staticmethod\n    async def test_reaction_to_flat_trend(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_flat_trend(\n            # eval_start_move_ending_up_in_a_rise,\n            0.75,\n            # eval_reaches_flat_trend, eval_first_micro_up_p1, eval_first_micro_up_p2,\n            0.6, 0.7, 0.45,\n            # eval_micro_down1, eval_micro_up1, eval_micro_down2, eval_micro_up2,\n            -0.1, -0.6, -0.55, -0.4,\n            # eval_micro_down3, eval_back_normal3, eval_micro_down4, eval_back_normal4,\n            -0.25, -0.1, -0.1, 0.2,\n            # eval_micro_down5, eval_back_up5, eval_micro_up6, eval_back_down6,\n            -0.5, -0.6, 0.24, 0.35,\n            # eval_back_normal6, eval_micro_down7, eval_back_up7, eval_micro_down8,\n            0.49, -0.1, -0.4, -0.26,\n            # eval_back_up8, eval_micro_down9, eval_back_up9\n            -0.31, -0.7, 0.1)\n"
  },
  {
    "path": "Evaluator/TA/momentum_evaluator/tests/test_rsi_TA_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport pytest_asyncio\n\nimport tests.functional_tests.evaluators_tests.abstract_TA_test as abstract_TA_test\nimport tentacles.Evaluator.TA as TA\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def evaluator_tester():\n    evaluator_tester_instance = TestRSIEvaluator()\n    evaluator_tester_instance.TA_evaluator_class = TA.RSIMomentumEvaluator\n    return evaluator_tester_instance\n\n\nclass TestRSIEvaluator(abstract_TA_test.AbstractTATest):\n\n    @staticmethod\n    async def test_stress_test(evaluator_tester):\n        await evaluator_tester.run_stress_test_without_exceptions(0.7, False)\n\n    @staticmethod\n    async def test_reactions_to_dump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_dump(0.3, -0.2, -0.8, -1, -1)\n\n    @staticmethod\n    async def test_reactions_to_pump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_pump(0.3, 0.6, 1, 1, 1, 1, 0.5)\n\n    @staticmethod\n    async def test_reaction_to_rise_after_over_sold(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_rise_after_over_sold(-1, -1, -1, -1, -0.7)\n\n    @staticmethod\n    async def test_reaction_to_over_bought_then_dip(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_over_bought_then_dip(0.1, 0.4, 0.85, 1, 0.75, 0.8)\n\n    @staticmethod\n    async def test_reaction_to_flat_trend(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_flat_trend(\n            # eval_start_move_ending_up_in_a_rise,\n            0.4,\n            # eval_reaches_flat_trend, eval_first_micro_up_p1, eval_first_micro_up_p2,\n            0.55, 0.9, 1,\n            # eval_micro_down1, eval_micro_up1, eval_micro_down2, eval_micro_up2,\n            0.5, 0.8, 1, 0.7,\n            # eval_micro_down3, eval_back_normal3, eval_micro_down4, eval_back_normal4,\n            0.55, -0.1, 0.75, 0,\n            # eval_micro_down5, eval_back_up5, eval_micro_up6, eval_back_down6,\n            0.2, -0.6, -0.45, 0.1,\n            # eval_back_normal6, eval_micro_down7, eval_back_up7, eval_micro_down8,\n            0, 0.75, 0.25, 0,\n            # eval_back_up8, eval_micro_down9, eval_back_up9\n            -1, -1, -0.75)\n"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/__init__.py",
    "content": "from .trend import DoubleMovingAverageTrendEvaluator, EMADivergenceTrendEvaluator, DeathAndGoldenCrossEvaluator, SuperTrendEvaluator"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/config/DeathAndGoldenCrossEvaluator.json",
    "content": "{\n    \"fast_length\": 50,\n    \"slow_length\": 200,\n    \"slow_ma_type\": \"SMA\",\n    \"fast_ma_type\": \"SMA\"\n}"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/config/DoubleMovingAverageTrendEvaluator.json",
    "content": "{\n    \"long_period_length\": 10,\n    \"short_period_length\": 5\n}"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/config/EMADivergenceTrendEvaluator.json",
    "content": "{\n    \"size\": 50,\n    \"short\": -2,\n    \"long\": 2\n}"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/config/SuperTrendEvaluator.json",
    "content": "{\n\t\"factor\": 3,\n\t\"length\": 10\n}"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"DoubleMovingAverageTrendEvaluator\", \"EMADivergenceTrendEvaluator\", \"DeathAndGoldenCrossEvaluator\", \"SuperTrendEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/resources/DeathAndGoldenCrossEvaluator.md",
    "content": "DeathAndGoldenCrossEvaluator is based on two [moving averages](https://www.investopedia.com/terms/m/movingaverage.asp), by default one of **50** periods and other one of **200**.\n\nIf the fast moving average is above the slow moving average, this indicates a bull market (signal: -1) When this happens it's called a [Golden Cross](https://www.investopedia.com/terms/g/goldencross.asp).\nInversely, if it's the fast moving average which is above the slow moving average this indicates a bear market (signal: 1). When this happens it's called a [Death Cross](https://www.investopedia.com/terms/d/deathcross.asp)\n\nThis evaluator will always produce a value of `0` except right after a golden or death cross \nis found, in this case a `-1` or `1` value will be produced.\n"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/resources/DoubleMovingAverageTrendEvaluator.md",
    "content": "Uses two [moving averages](https://www.investopedia.com/terms/m/movingaverage.asp) (a slow and a fast one) to find reversals.\n\nEvaluates from -1 to 1 relatively to the computed reversal probability and the current price distance from \n[moving averages](https://www.investopedia.com/terms/m/movingaverage.asp)."
  },
  {
    "path": "Evaluator/TA/trend_evaluator/resources/EMADivergenceTrendEvaluator.md",
    "content": "Uses [exponential moving averages](https://www.investopedia.com/terms/e/ema.asp) to find price divergences.\n\nEvaluates from -1 to 1 relatively to the computed divergence strength."
  },
  {
    "path": "Evaluator/TA/trend_evaluator/resources/SuperTrendEvaluator.md",
    "content": "SuperTrendEvaluator is a trend-following indicator based on Average True Range [ATR](https://www.tradingview.com/scripts/averagetruerange/). The calculation of its single line combines trend detection and volatility. It can be used to detect changes in trend direction and to position stops.\n\nEvaluates -1 on an upwards trend and 1 if the trend is downwards.\n"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/tests/test_double_moving_averages_TA_evaluator.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport pytest_asyncio\n\n\nfrom tests.functional_tests.evaluators_tests.abstract_TA_test import AbstractTATest\nfrom tentacles.Evaluator.TA import DoubleMovingAverageTrendEvaluator\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def evaluator_tester():\n    evaluator_tester_instance = TestDoubleMovingAveragesEvaluator()\n    evaluator_tester_instance.TA_evaluator_class = DoubleMovingAverageTrendEvaluator\n    return evaluator_tester_instance\n\n\nclass TestDoubleMovingAveragesEvaluator(AbstractTATest):\n\n    @staticmethod\n    async def test_stress_test(evaluator_tester):\n        await evaluator_tester.run_stress_test_without_exceptions(0.8)\n\n    @staticmethod\n    async def test_reactions_to_dump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_dump(0.15, 0.15, -0.35, -0.75, -1)\n\n    @staticmethod\n    async def test_reactions_to_pump(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_pump(0.1, 0.4, 1, 1, 1, 0.96, -0.45)\n\n    @staticmethod\n    async def test_reaction_to_rise_after_over_sold(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_rise_after_over_sold(-0.7, -0.99, -0.99, -0.5, 0.85)\n\n    @staticmethod\n    async def test_reaction_to_over_bought_then_dip(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_over_bought_then_dip(0, 0.4, 0.7, 0.6, -0.88, -0.1)\n\n    @staticmethod\n    async def test_reaction_to_flat_trend(evaluator_tester):\n        await evaluator_tester.run_test_reactions_to_flat_trend(\n            # eval_start_move_ending_up_in_a_rise,\n            0.45,\n            # eval_reaches_flat_trend, eval_first_micro_up_p1, eval_first_micro_up_p2,\n            1, 0.65, 0.2,\n            # eval_micro_down1, eval_micro_up1, eval_micro_down2, eval_micro_up2,\n            -0.25, 0, -0.1, 0,\n            # eval_micro_down3, eval_back_normal3, eval_micro_down4, eval_back_normal4,\n            -0.1, 0, -0.1, 0,\n            # eval_micro_down5, eval_back_up5, eval_micro_up6, eval_back_down6,\n            0.2, -0.10, 0, 0.1,\n            # eval_back_normal6, eval_micro_down7, eval_back_up7, eval_micro_down8,\n            -0.05, -0.1, -0.1, -0.15,\n            # eval_back_up8, eval_micro_down9, eval_back_up9\n            0, -0.1, 0.1)\n"
  },
  {
    "path": "Evaluator/TA/trend_evaluator/trend.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport math\n\nimport tulipy\nimport numpy\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as enums\nimport octobot_commons.data_util as data_util\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.util as evaluators_util\nimport octobot_trading.api as trading_api\nimport tentacles.Evaluator.Util as EvaluatorUtil\n\n\nclass SuperTrendEvaluator(evaluators.TAEvaluator):\n    FACTOR = \"factor\"\n    LENGTH = \"length\"\n    PREV_UPPER_BAND = \"prev_upper_band\"\n    PREV_LOWER_BAND = \"prev_lower_band\"\n    PREV_SUPERTREND = \"prev_supertrend\"\n    PREV_ATR = \"prev_atr\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.factor = 3\n        self.length = 10\n        self.reversals_only = False\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        self.previous_value = {}\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the evaluator, should define all the evaluator's user inputs\n        \"\"\"\n        self.factor = self.UI.user_input(\"factor\", enums.UserInputTypes.FLOAT, self.factor,\n                                         inputs, min_val=0, title=\"Factor multiplier of the ATR\")\n        self.length = self.UI.user_input(\"length\", enums.UserInputTypes.INT, self.length,\n                                         inputs, min_val=1, title=\"Length of the ATR\")\n        self.reversals_only = self.UI.user_input(\n            \"reversals_only\", enums.UserInputTypes.BOOLEAN, self.reversals_only, inputs, \n            title=\"Reversals only: evaluates -1 and 1 only on trend reversals, 0 otherwise\"\n        )\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str, cryptocurrency: str,\n                             symbol: str, time_frame, candle, inc_in_construction_data):\n        exchange_symbol_data = self.get_exchange_symbol_data(exchange, exchange_id, symbol)\n        high = trading_api.get_symbol_high_candles(exchange_symbol_data, time_frame,\n                                                   include_in_construction=inc_in_construction_data)\n        low = trading_api.get_symbol_low_candles(exchange_symbol_data, time_frame,\n                                                 include_in_construction=inc_in_construction_data)\n        close = trading_api.get_symbol_close_candles(exchange_symbol_data, time_frame,\n                                                     include_in_construction=inc_in_construction_data)\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(close) > self.length:\n            await self.evaluate(cryptocurrency, symbol, time_frame, candle, high, low, close)\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle, high, low, close):\n        hl2 = EvaluatorUtil.CandlesUtil.HL2(high, low)[-1]\n        atr = tulipy.atr(high, low, close, self.length)[-1]\n\n        previous_value = self.get_previous_value(symbol, time_frame)\n\n        upper_band = hl2 + self.factor * atr\n        lower_band = hl2 - self.factor * atr\n        prev_upper_band = previous_value.get(self.PREV_UPPER_BAND, 0)\n        prev_lower_band = previous_value.get(self.PREV_LOWER_BAND, 0)\n\n        # compute latest lower and upper band values\n        latest_lower_band = lower_band if (lower_band > prev_lower_band or close[-2] < prev_lower_band) else prev_lower_band\n        latest_upper_band = upper_band if (upper_band < prev_upper_band or close[-2] > prev_upper_band) else prev_upper_band\n\n        prev_super_trend = previous_value.get(self.PREV_SUPERTREND, 0)\n\n        signal = -1\n        is_reversal = False\n        if previous_value.get(self.PREV_ATR, None) is None:\n            # not enough data to compute supertrend evaluation\n            signal = -1\n        else:\n            # there is a previous value: check if the latest close is above or below ATR\n            # and select the correct band to use\n            if prev_super_trend == prev_upper_band:\n                # previous bearish trend: previous super trend used the upper band \n                # bullish if the latest close is above latest upper band\n                bullish_switch = close[-1] > latest_upper_band\n                if bullish_switch:\n                    # bullish switch of the trend\n                    signal = -1\n                    is_reversal = True\n                else:\n                    # bearish continuation of the trend\n                    signal = 1\n            else:\n                # previous bullish trend: previous super trend used the lower band\n                # bearsish if the latest close is bellow latest lower band\n                bearish_switch = close[-1] < latest_lower_band\n                if bearish_switch:\n                    # bearish switch of the trend\n                    signal = 1\n                    is_reversal = True\n                else:\n                    # bullish continuation of the trend\n                    signal = -1\n\n        previous_value[self.PREV_ATR] = atr\n        previous_value[self.PREV_UPPER_BAND] = latest_upper_band\n        previous_value[self.PREV_LOWER_BAND] = latest_lower_band\n        # store the latest used super trend band: bullish = lower band, bearish = upper band\n        previous_value[self.PREV_SUPERTREND] = latest_lower_band if signal == -1 else latest_upper_band\n        self.eval_note = signal if is_reversal or not self.reversals_only else commons_constants.START_PENDING_EVAL_NOTE\n\n    def get_previous_value(self, symbol, time_frame):\n        try:\n            previous_symbol_value = self.previous_value[symbol]\n        except KeyError:\n            self.previous_value[symbol] = {}\n            previous_symbol_value = self.previous_value[symbol]\n        try:\n            return previous_symbol_value[time_frame]\n        except KeyError:\n            previous_symbol_value[time_frame] = {}\n            return previous_symbol_value[time_frame]\n\n\nclass DeathAndGoldenCrossEvaluator(evaluators.TAEvaluator):\n    FAST_LENGTH = \"fast_length\"\n    SLOW_LENGTH = \"slow_length\"\n    SLOW_MA_TYPE = \"slow_ma_type\"\n    FAST_MA_TYPE = \"fast_ma_type\"\n    MA_TYPES = [\"EMA\", \"WMA\", \"SMA\", \"LSMA\", \"KAMA\", \"DEMA\", \"TEMA\", \"VWMA\"]\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.fast_length = 50\n        self.slow_length = 200\n        self.fast_ma_type = \"sma\"\n        self.slow_ma_type = \"sma\"\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the evaluator, should define all the evaluator's user inputs\n        \"\"\"\n        self.fast_length = self.UI.user_input(self.FAST_LENGTH, enums.UserInputTypes.INT, self.fast_length,\n                                              inputs, min_val=1, title=\"Fast MA length\")\n        self.slow_length = self.UI.user_input(self.SLOW_LENGTH, enums.UserInputTypes.INT, self.slow_length,\n                                              inputs, min_val=1, title=\"Slow MA length\")\n        self.fast_ma_type = self.UI.user_input(self.FAST_MA_TYPE, enums.UserInputTypes.OPTIONS, self.fast_ma_type,\n                                               inputs, options=self.MA_TYPES, title=\"Fast MA type\").lower()\n        self.slow_ma_type = self.UI.user_input(self.SLOW_MA_TYPE, enums.UserInputTypes.OPTIONS, self.slow_ma_type,\n                                               inputs, options=self.MA_TYPES, title=\"Slow MA type\").lower()\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n\n        close = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                     time_frame,\n                                                     include_in_construction=inc_in_construction_data)\n        volume = trading_api.get_symbol_volume_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                       time_frame,\n                                                       include_in_construction=inc_in_construction_data)\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(close) > max(self.slow_length, self.fast_length):\n            await self.evaluate(cryptocurrency, symbol, time_frame, candle, close, volume)\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle, candle_data, volume_data):\n        if self.fast_ma_type == \"vwma\":\n            fast_ma = tulipy.vwma(candle_data, volume_data, self.fast_length)\n        elif self.fast_ma_type == \"lsma\":\n            fast_ma = tulipy.linreg(candle_data, self.fast_length)\n        else:\n            fast_ma = getattr(tulipy, self.fast_ma_type)(candle_data, self.fast_length)\n\n        if self.slow_ma_type == \"vwma\":\n            slow_ma = tulipy.vwma(candle_data, volume_data, self.slow_length)\n        elif self.slow_ma_type == \"lsma\":\n            slow_ma = tulipy.linreg(candle_data, self.slow_length)\n        else:\n            slow_ma = getattr(tulipy, self.slow_ma_type)(candle_data, self.slow_length)\n\n        if min(len(fast_ma), len(slow_ma)) < 2:\n            # can't compute crosses: not enough data\n            self.logger.debug(f\"Not enough data to compute crosses, skipping {symbol} {time_frame} evaluation\")\n            return\n\n        just_crossed = (\n            fast_ma[-1] > slow_ma[-1] and fast_ma[-2] < slow_ma[-2]\n        ) or (\n            fast_ma[-1] < slow_ma[-1] and fast_ma[-2] > slow_ma[-2]\n        )\n        if just_crossed:\n            # crosses happen when the fast_ma and fast_ma just crossed, therefore when it happened on the last candle\n            if fast_ma[-1] > slow_ma[-1]:\n                # golden cross\n                self.eval_note = -1\n            elif fast_ma[-1] < slow_ma[-1]:\n                # death cross\n                self.eval_note = 1\n\n\n# evaluates position of the current (2 unit) average trend relatively to the 5 units average and 10 units average trend\nclass DoubleMovingAverageTrendEvaluator(evaluators.TAEvaluator):\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.slow_period_length = 10\n        self.fast_period_length = 5\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the evaluator, should define all the evaluator's user inputs\n        \"\"\"\n        self.slow_period_length = self.UI.user_input(\"long_period_length\", enums.UserInputTypes.INT,\n                                                     self.slow_period_length,\n                                                     inputs, min_val=1, title=\"Slow SMA length\")\n        self.fast_period_length = self.UI.user_input(\"short_period_length\", enums.UserInputTypes.INT,\n                                                     self.fast_period_length,\n                                                     inputs, min_val=1, title=\"Fast SMA length\")\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(candle_data) >= max(self.slow_period_length, self.fast_period_length):\n            current_moving_average = tulipy.sma(candle_data, 2)\n            results = [self.get_moving_average_analysis(candle_data, current_moving_average, time_unit)\n                       for time_unit in (self.fast_period_length, self.slow_period_length)]\n            if len(results):\n                self.eval_note = numpy.mean(results)\n            else:\n                self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n\n            if self.eval_note == 0:\n                self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n\n    # < 0 --> Current average bellow other one (computed using time_period)\n    # > 0 --> Current average above other one (computed using time_period)\n    @staticmethod\n    def get_moving_average_analysis(data, current_moving_average, time_period):\n\n        time_period_unit_moving_average = tulipy.sma(data, time_period)\n\n        # equalize array size\n        min_len_arrays = min(len(time_period_unit_moving_average), len(current_moving_average))\n\n        # compute difference between 1 unit values and others ( >0 means currently up the other one)\n        values_difference = \\\n            (current_moving_average[-min_len_arrays:] - time_period_unit_moving_average[-min_len_arrays:])\n        values_difference = data_util.drop_nan(values_difference)\n\n        if len(values_difference):\n            # indexes where current_unit_moving_average crosses time_period_unit_moving_average\n            crossing_indexes = EvaluatorUtil.TrendAnalysis.get_threshold_change_indexes(values_difference, 0)\n\n            multiplier = 1 if values_difference[-1] > 0 else -1\n\n            # check at least some data crossed 0\n            if crossing_indexes:\n                normalized_data = data_util.normalize_data(values_difference)\n                current_value = min(abs(normalized_data[-1]) * 2, 1)\n                if math.isnan(current_value):\n                    return 0\n                # check <= values_difference.count()-1if current value is max/min\n                if current_value == 0 or current_value == 1:\n                    chances_to_be_max = EvaluatorUtil.TrendAnalysis.get_estimation_of_move_state_relatively_to_previous_moves_length(\n                        crossing_indexes,\n                        values_difference)\n                    return multiplier * current_value * chances_to_be_max\n                # other case: maxima already reached => return distance to max\n                else:\n                    return multiplier * current_value\n\n        # just crossed the average => neutral\n        return 0\n\n\n# evaluates position of the current ema to detect divergences\nclass EMADivergenceTrendEvaluator(evaluators.TAEvaluator):\n    EMA_SIZE = \"size\"\n    SHORT_VALUE = \"short\"\n    LONG_VALUE = \"long\"\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.period = 50\n        self.long_value = 2\n        self.short_value = -2\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the evaluator, should define all the evaluator's user inputs\n        \"\"\"\n        self.period = self.UI.user_input(self.EMA_SIZE, enums.UserInputTypes.INT, self.period,\n                                         inputs, min_val=1, title=\"EMA period length\")\n        self.long_value = self.UI.user_input(\"long_value\", enums.UserInputTypes.INT, self.long_value,\n                                             inputs, title=\"Long threshold: Minimum % price difference from EMA \"\n                                                           \"consider a long signal. Should be positive in most cases\")\n        self.short_value = self.UI.user_input(\"short_value\", enums.UserInputTypes.INT, self.short_value,\n                                              inputs, title=\"Short threshold: Minimum % price difference from EMA \"\n                                                            \"consider a short signal. Should be negative in most cases\")\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        if len(candle_data) >= self.period:\n            current_ema = tulipy.ema(candle_data, self.period)[-1]\n            current_price_close = candle_data[-1]\n            diff = (current_price_close / current_ema * 100) - 100\n\n            if diff <= self.long_value:\n                self.eval_note = -1\n            elif diff >= self.short_value:\n                self.eval_note = 1\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n"
  },
  {
    "path": "Evaluator/TA/volatility_evaluator/__init__.py",
    "content": "from .volatility import StochasticRSIVolatilityEvaluator"
  },
  {
    "path": "Evaluator/TA/volatility_evaluator/config/StochasticRSIVolatilityEvaluator.json",
    "content": "{\n    \"period\": 14,\n    \"low_level\": 1,\n    \"high_level\": 98\n}"
  },
  {
    "path": "Evaluator/TA/volatility_evaluator/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"StochasticRSIVolatilityEvaluator\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/TA/volatility_evaluator/resources/StochasticRSIVolatilityEvaluator.md",
    "content": "Uses the [Stochastic RSI](https://www.investopedia.com/terms/s/stochrsi.asp) as a volatilty evaluator to identify trends.\n\nWhen found, evaluates from -1 to 1 according to the strength of the trend."
  },
  {
    "path": "Evaluator/TA/volatility_evaluator/volatility.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tulipy\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as enums\nimport octobot_commons.data_util as data_util\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.util as evaluators_util\nimport octobot_trading.api as trading_api\n\n\nclass StochasticRSIVolatilityEvaluator(evaluators.TAEvaluator):\n    STOCHRSI_PERIOD = \"period\"\n    HIGH_LEVEL = \"high_level\"\n    LOW_LEVEL = \"low_level\"\n    TULIPY_INDICATOR_MULTIPLICATOR = 100\n\n    def __init__(self, tentacles_setup_config):\n        super().__init__(tentacles_setup_config)\n        self.period = 14\n        self.low_level = 1\n        self.high_level = 98\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        self.period = self.UI.user_input(self.STOCHRSI_PERIOD, enums.UserInputTypes.INT,\n                                         self.period, inputs, min_val=2,\n                                         title=\"Period: length of the stochastic RSI period.\")\n        self.low_level = self.UI.user_input(self.LOW_LEVEL, enums.UserInputTypes.FLOAT,\n                                            self.low_level, inputs, min_val=0,\n                                            title=\"Low threshold: stochastic RSI level from which evaluation \"\n                                                  \"is considered a buy signal.\")\n        self.high_level = self.UI.user_input(self.HIGH_LEVEL, enums.UserInputTypes.FLOAT,\n                                             self.high_level, inputs, min_val=0,\n                                             title=\"High threshold: stochastic RSI level from which evaluation \"\n                                                   \"is considered a sell signal.\")\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str,\n                             cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):\n        candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),\n                                                           time_frame,\n                                                           include_in_construction=inc_in_construction_data)\n        await self.evaluate(cryptocurrency, symbol, time_frame, candle_data, candle)\n\n    async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle):\n        try:\n            if len(candle_data) >= self.period * 2:\n                stochrsi_value = tulipy.stochrsi(data_util.drop_nan(candle_data), self.period)[-1]\n\n                if stochrsi_value * self.TULIPY_INDICATOR_MULTIPLICATOR >= self.high_level:\n                    self.eval_note = 1\n                elif stochrsi_value * self.TULIPY_INDICATOR_MULTIPLICATOR <= self.low_level:\n                    self.eval_note = -1\n                else:\n                    self.eval_note = stochrsi_value - 0.5\n        except tulipy.lib.InvalidOptionError as e:\n            message = \"\"\n            if self.period <= 1:\n                message = \" period should be higher than 1.\"\n            self.logger.warning(f\"Error when computing StochasticRSIVolatilityEvaluator: {e}{message}\")\n            self.logger.exception(e, False)\n            self.eval_note = commons_constants.START_PENDING_EVAL_NOTE\n        await self.evaluation_completed(cryptocurrency, symbol, time_frame,\n                                        eval_time=evaluators_util.get_eval_time(full_candle=candle,\n                                                                                time_frame=time_frame))\n"
  },
  {
    "path": "Evaluator/Util/candles_util/__init__.py",
    "content": "from .candles_util import CandlesUtil"
  },
  {
    "path": "Evaluator/Util/candles_util/candles_util.pxd",
    "content": "# cython: language_level=3\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\ncimport numpy as np\nfrom math cimport mean\n\ncpdef object HL2(object high, object low)\ncpdef object HLC3(object high, object low, object close)\ncpdef object OHLC4(object open, object high, object low, object close)\ncpdef tuple HeikinAshi(object open, object high, object low, object close)"
  },
  {
    "path": "Evaluator/Util/candles_util/candles_util.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport numpy as np\nfrom octobot_commons.data_util import mean\n\nclass CandlesUtil:\n\n    @staticmethod\n    def HL2(candles_high, candles_low):\n        \"\"\"\n        Return a list of HL2 value (high + low ) / 2\n        :param high: list of high\n        :param low: list of low\n        :return: list of HL2\n        \"\"\"\n        return np.array(list(map((lambda candles_high, candles_low: mean([candles_high, candles_low])),\n                                                                candles_high, candles_low)))\n\n    @staticmethod\n    def HLC3(candles_high, candles_low, candles_close):\n        \"\"\"\n        Return a list of HLC3 values (high + low + close) / 3\n        :param high: list of high\n        :param low: list of low\n        :param close: list of close\n        :return: list of HLC3\n        \"\"\"\n        return np.array(list(map((lambda candles_high, candles_low, candles_close:\n                            mean([candles_high, candles_low, candles_close])),\n                            candles_high, candles_low, candles_close)))\n\n    @staticmethod\n    def OHLC4(candles_open, candles_high, candles_low, candles_close):\n        \"\"\"\n        Return a list of OHLC4 value (open + high + low + close) / 4\n        :param open: list of open\n        :param high: list of high\n        :param low: list of low\n        :param close: list of close\n        :return: list of OHLC4\n        \"\"\"\n        return np.array(list(map((lambda candles_open, candles_high, candles_low, candles_close:\n                            mean([candles_open, candles_high, candles_low, candles_close])),\n                            candles_open, candles_high, candles_low, candles_close)))\n\n    @staticmethod\n    def HeikinAshi(candles_open, candles_high, candles_low, candles_close):\n        \"\"\"\n        Return HeikinAshi array of the given candles\n        :param open: list of open\n        :param high: list of high\n        :param low: list of low\n        :param close: list of close\n        :return: HAopen, HAhigh, HAlow, HAclose\n        \"\"\"\n        haOpen, haHigh, haLow, haClose = [np.array([]) for i in range(4)]\n        for i, (open_value, high_value, low_value, close_value) \\\n                            in enumerate(zip(candles_open, candles_high, candles_low, candles_close)):\n            if i == 0:\n                haOpen = np.append(haOpen, open_value)\n                haHigh = np.append(haHigh, high_value)\n                haLow = np.append(haLow, low_value)\n                haClose = np.append(haClose, close_value)\n                continue\n            haOpen = np.append(haOpen, mean([candles_open[i-1], candles_close[i-1]]))\n            haHigh = np.append(haHigh, high_value)\n            haLow = np.append(haLow, low_value)\n            haClose = np.append(haClose, mean([open_value, high_value, low_value, close_value]))\n        return haOpen, haHigh, haLow, haClose"
  },
  {
    "path": "Evaluator/Util/candles_util/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CandlesUtil\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Util/candles_util/tests/test_candles_util.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport numpy as np\n\nfrom tentacles.Evaluator.Util import CandlesUtil\n\n\ndef test_HL2():\n    candles_high = np.array([10, 12, np.nan, 45, 5.67, 6.54, 75, 8.01, 9])\n    candles_low = np.array([9, 8, 7, 6, 5, 4, 3, 2, 1])\n    np.testing.assert_array_equal(CandlesUtil.HL2(candles_high, candles_low),\n                                  np.array([9.5, 10, np.nan, 25.5, 5.335, 5.27, 39.0, 5.005, 5.0], dtype=np.float64))\n\n    candles_high = np.array([120, 123, 54, 45, 210.54, 546.21, 981.2, .958, 65.7])\n    candles_low = np.array([887.592, 896.519, 97.416, 233.987, 846.789, 713.054, 856.985, 421.17, 874.296])\n    np.testing.assert_array_equal(CandlesUtil.HL2(candles_high, candles_low),\n                                    np.array([503.796, 509.7595, 75.708, 139.49349999999998, 528.6645, 629.6320000000001,\n                                    919.0925, 211.06400000000002, 469.99800000000005], dtype=np.float64))\n\ndef test_HLC3():\n    candles_high = np.array([9, 13, np.nan, 45, 5.67, 6.54, 75, 8.01, 9])\n    candles_low = np.array([19, 25, 17, 36, 45, 84, 31, 21, 10])\n    candles_close = np.array([2, 4, 4, 4, 6, 7, 8, 9, 10])\n    np.testing.assert_array_equal(CandlesUtil.HLC3(candles_high, candles_low, candles_close),\n                                  np.array([10, 14, np.nan, 28.333333333333332, 18.89,\n                                  32.513333333333335, 38, 12.67, 9.666666666666666], dtype=np.float64))\n\n    candles_high = np.array([733.985, 86.751, 388.834, 630.849, 231.102, 224.815, 430.74, 776.919, 209.207])\n    candles_low = np.array([145.747, 829.698, 534.426, 879.53, 187.895, 698.515, 822.942, 532.641, 626.917])\n    candles_close = np.array([811.199, 278.313, 817.295, 315.199, 974.104, 775.321, 979.139, 790.477, 518.736])\n    np.testing.assert_array_equal(CandlesUtil.HLC3(candles_high, candles_low, candles_close),\n                                  np.array([563.6436666666667, 398.25399999999996, 580.185, 608.526, 464.367,\n                                  566.217, 744.2736666666666, 700.0123333333332, 451.62000000000006], dtype=np.float64))\n\ndef test_OHLC4():\n    candles_open = np.array([251.613, 259.098, 247.819, 140.73, 237.547, 830.611, 433.168, 404.026, 403.538])\n    candles_high = np.array([980.99, 403.92, 698.072, 658.647, 245.151, 480.9, 621.35, 429.109, 637.439])\n    candles_low = np.array([658.777, 101.13, 549.588, 28.624, 132.07, 813.572, 366.478, 619.649, 371.696])\n    candles_close = np.array([812.829, 880.456, 406.039, 39.224, 917.386, 707.281, 737.851, 330.262, 258.689])\n    np.testing.assert_array_equal(CandlesUtil.OHLC4(candles_open, candles_high, candles_low, candles_close),\n                                    np.array([676.05225, 411.151, 475.37949999999995, 216.80625000000003,\n                                    383.0385, 708.091, 539.71175, 445.7615, 417.84049999999996], dtype=np.float64))\n\n    candles_open = np.array([345.468, 484.778, 332.855, 401.893, 41.936, 333.738, 983.158, 996.979, 807.855])\n    candles_high = np.array([547.277, 856.206, 439.542, 921.475, 778.994, 156.285, 653.31, 534.865, 427.64])\n    candles_low = np.array([328.444, 593.535, 4.243, 83.902, 811.859, 396.442, 433.552, 127.624, 314.613])\n    candles_close = np.array([905.792, 382.98, 135.529, 494.942, 510.52, 399.78, 897.088, 192.068, 771.189])\n    np.testing.assert_array_equal(CandlesUtil.OHLC4(candles_open, candles_high, candles_low, candles_close),\n                                        np.array([531.74525, 579.37475, 228.04225, 475.553, 535.82725,\n                                        321.56125, 741.777, 462.884, 580.32425], dtype=np.float64))\n\ndef test_HeikinAshi():\n    candles_open = np.array([977.88, 573.634, 816.233, 846.748, 184.114, 35.742, 598.653, 745.916, 854.334])\n    candles_high = np.array([4.757, 499.759, 602.794, 179.313, 802.019, 384.307, 637.378, 161.048, 366.51])\n    candles_low = np.array([903.152, 877.832, 966.154, 104.582, 837.638, 568.788, 788.584, 510.926, 608.184])\n    candles_close = np.array([405.527, 685.962, 495.698, 271.687, 573.667, 891.018, 445.342, 344.928, 894.279])\n\n    haOpen, haHigh, haLow, haClose = CandlesUtil.HeikinAshi(candles_open, candles_high, candles_low, candles_close)\n    np.testing.assert_array_equal(haOpen, np.array([977.88, 691.7035, 629.798, 655.9655, 559.2175,\n                                                378.89050000000003, 463.38, 521.9975, 545.422], dtype=np.float64))\n    np.testing.assert_array_equal(haHigh, np.array([4.757, 499.759, 602.794, 179.313, 802.019,\n                                                384.307, 637.378, 161.048, 366.51], dtype=np.float64))\n    np.testing.assert_array_equal(haLow, np.array([903.152, 877.832, 966.154, 104.582, 837.638,\n                                                568.788, 788.584, 510.926, 608.184], dtype=np.float64))\n    np.testing.assert_array_equal(haClose, np.array([405.527, 659.29675, 720.21975, 350.5825, 599.3595,\n                                                469.96375, 617.48925, 440.70450000000005, 680.82675], dtype=np.float64))\n\n    candles_open = np.array([188.539, 334.682, 495.604, 638.736, 632.213, 705.675, 876.735, 69.951, 909.477])\n    candles_high = np.array([259.316, 843.705, 170.388, 318.961, 918.236, 585.595, 23.266, 657.422, 270.557])\n    candles_low = np.array([652.361, 293.607, 295.191, 893.255, 819.447, 647.016, 330.303, 472.415, 617.705])\n    candles_close = np.array([968.007, 114.792, 680.216, 168.147, 478.577, 437.676, 299.474, 208.601, 333.237])\n\n    haOpen, haHigh, haLow, haClose = CandlesUtil.HeikinAshi(candles_open, candles_high, candles_low, candles_close)\n    np.testing.assert_array_equal(haOpen, np.array([188.539, 578.2729999999999, 224.73700000000002, 587.91,\n                                                403.4415, 555.395, 571.6754999999999, 588.1045, 139.276], dtype=np.float64))\n    np.testing.assert_array_equal(haHigh, np.array([259.316, 843.705, 170.388, 318.961, 918.236, 585.595,\n                                                23.266, 657.422, 270.557], dtype=np.float64))\n    np.testing.assert_array_equal(haLow, np.array([652.361, 293.607, 295.191, 893.255, 819.447, 647.016,\n                                                330.303, 472.415, 617.705], dtype=np.float64))\n    np.testing.assert_array_equal(haClose, np.array([968.007, 396.6965, 410.34975, 504.77475, 712.11825,\n                                                593.9905, 382.4445, 352.09725000000003, 532.744], dtype=np.float64))"
  },
  {
    "path": "Evaluator/Util/overall_state_analysis/__init__.py",
    "content": "from .overall_state_analysis import OverallStateAnalyser"
  },
  {
    "path": "Evaluator/Util/overall_state_analysis/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"OverallStateAnalyser\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Util/overall_state_analysis/overall_state_analysis.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport numpy\n\nimport octobot_commons.constants as commons_constants\n\n\nclass OverallStateAnalyser:\n    def __init__(self):\n        self.overall_state = commons_constants.START_PENDING_EVAL_NOTE\n        self.evaluation_count = 0\n        self.evaluations = []\n\n    # evaluation: number between -1 and 1\n    # weight: integer between 0 (not even taken into account) and X\n    def add_evaluation(self, evaluation, weight, refresh_overall_state=True):\n        self.evaluations.append(StateEvaluation(evaluation, weight))\n        if refresh_overall_state:\n            self._refresh_overall_state()\n\n    def get_overall_state_after_refresh(self, refresh_overall_state=True):\n        if refresh_overall_state:\n            self._refresh_overall_state()\n        return self.overall_state\n\n    # computes self.overall_state using self.evaluations values and weights\n    def _refresh_overall_state(self):\n        if self.evaluations:\n            self.overall_state = numpy.mean(\n                [evaluation.value for evaluation in self.evaluations for _ in range(evaluation.weight)]\n            )\n\n\nclass StateEvaluation:\n    def __init__(self, value, weight):\n        self.value = value\n        self.weight = weight\n"
  },
  {
    "path": "Evaluator/Util/pattern_analysis/__init__.py",
    "content": "from .pattern_analysis import PatternAnalyser"
  },
  {
    "path": "Evaluator/Util/pattern_analysis/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"PatternAnalyser\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Util/pattern_analysis/pattern_analysis.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport numpy as np\nimport math\n\n\nclass PatternAnalyser:\n\n    UNKNOWN_PATTERN = \"?\"\n\n    # returns the starting and ending index of the pattern if it's found\n    # supported patterns:\n    # W, M, N and V (ex: for macd)\n    # return boolean (pattern found or not), start index and end index\n    @staticmethod\n    def find_pattern(data, zero_crossing_indexes, data_frame_max_index):\n        if len(zero_crossing_indexes) > 1:\n\n            last_move_data = data[zero_crossing_indexes[-1]:]\n\n            # if last_move_data is shaped in W\n            shape = PatternAnalyser.get_pattern(last_move_data)\n\n            if shape == \"N\" or shape == \"V\":\n                # check presence of W or M with insignificant move in the other direction\n                backwards_index = 2\n                while backwards_index < len(zero_crossing_indexes) and \\\n                        zero_crossing_indexes[-1*backwards_index] - zero_crossing_indexes[-1*backwards_index-1] < 4:\n                    backwards_index += 1\n                extended_last_move_data = data[zero_crossing_indexes[-1 * backwards_index]:]\n                extended_shape = PatternAnalyser.get_pattern(extended_last_move_data)\n\n                if extended_shape == \"W\" or extended_shape == \"M\":\n                    # check that values are on the same side (< or >0)\n                    first_part = data[zero_crossing_indexes[-1 * backwards_index]:\n                                      zero_crossing_indexes[-1*backwards_index+1]]\n                    second_part = data[zero_crossing_indexes[-1]:]\n                    if np.mean(first_part)*np.mean(second_part) > 0:\n                        return extended_shape, zero_crossing_indexes[-1*backwards_index], zero_crossing_indexes[-1]\n\n            return shape, zero_crossing_indexes[-1], data_frame_max_index\n        else:\n            # if very few data: proceed with basic analysis\n\n            # if last_move_data is shaped in W\n            start_pattern_index = 0 if not zero_crossing_indexes else zero_crossing_indexes[0]\n            shape = PatternAnalyser.get_pattern(data[start_pattern_index:])\n            return shape, start_pattern_index, data_frame_max_index\n\n    @staticmethod\n    def get_pattern(data):\n        if len(data) > 0:\n            mean_value = np.mean(data) * 0.7\n        else:\n            mean_value = math.nan\n        if math.isnan(mean_value):\n            return PatternAnalyser.UNKNOWN_PATTERN\n        indexes_under_mean_value = np.where(data > mean_value)[0] \\\n            if mean_value < 0 \\\n            else np.where(data < mean_value)[0]\n\n        nb_gaps = 0\n        for i in range(len(indexes_under_mean_value)-1):\n            if indexes_under_mean_value[i+1]-indexes_under_mean_value[i] > 3:\n                nb_gaps += 1\n\n        if nb_gaps > 1:\n            return \"W\" if mean_value < 0 else \"M\"\n        else:\n            return \"V\" if mean_value < 0 else \"N\"\n\n    # returns a value 0 < value < 1: the higher the stronger is the pattern\n    @staticmethod\n    def get_pattern_strength(pattern):\n        if pattern == \"W\" or pattern == \"M\":\n            return 1\n        elif pattern == \"N\" or pattern == \"V\":\n            return 0.75\n        return 0\n"
  },
  {
    "path": "Evaluator/Util/statistics_analysis/__init__.py",
    "content": "from .statistics_analysis import StatisticAnalysis"
  },
  {
    "path": "Evaluator/Util/statistics_analysis/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"StatisticAnalysis\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Util/statistics_analysis/statistics_analysis.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tulipy\nimport numpy\n\nimport octobot_commons.constants as commons_constants\n\n\nclass StatisticAnalysis:\n\n    # Return linear proximity to the lower or the upper band relatively to the middle band.\n    # Linearly compute proximity between middle and delta before linear:\n    @staticmethod\n    def analyse_recent_trend_changes(data, delta_function):\n        # compute bollinger bands\n        lower_band, middle_band, upper_band = tulipy.bbands(data, 20, 2)\n        # if close to lower band => low value => bad,\n        # therefore if close to middle, value is keeping up => good\n        # finally if up the middle one or even close to the upper band => very good\n\n        current_value = data[-1]\n        current_up = upper_band[-1]\n        current_middle = middle_band[-1]\n        current_low = lower_band[-1]\n        delta_up = current_up - current_middle\n        delta_low = current_middle - current_low\n\n        # its exactly on all bands\n        if current_up == current_low:\n            return commons_constants.START_PENDING_EVAL_NOTE\n\n        # exactly on the middle\n        elif current_value == current_middle:\n            return 0\n\n        # up the upper band\n        elif current_value > current_up:\n            return -1\n\n        # down the lower band\n        elif current_value < current_low:\n            return 1\n\n        # delta given: use parabolic factor after delta, linear before\n        delta = delta_function(numpy.mean([delta_up, delta_low]))\n\n        micro_change = ((current_value / current_middle) - 1) / 2\n\n        # approximately on middle band\n        if current_middle + delta >= current_value >= current_middle - delta:\n            return micro_change\n\n        # up the middle area\n        elif current_middle + delta < current_value:\n            return -1 * max(micro_change, (current_value - current_middle) / delta_up)\n\n        # down the middle area\n        elif current_middle - delta > current_value:\n            return max(micro_change, (current_middle - current_value) / delta_low)\n\n        # should not happen\n        return 0\n"
  },
  {
    "path": "Evaluator/Util/text_analysis/__init__.py",
    "content": "from .text_analysis import TextAnalysis"
  },
  {
    "path": "Evaluator/Util/text_analysis/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TextAnalysis\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Util/text_analysis/text_analysis.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_commons.constants as commons_constants\ntry:\n    import vaderSentiment.vaderSentiment as vaderSentiment\nexcept ImportError:\n    if commons_constants.USE_MINIMAL_LIBS:\n        # mock vaderSentiment imports\n        class VaderSentimentImportMock:\n            class SentimentIntensityAnalyzer:\n                def __init__(self, *args):\n                    raise ImportError(\"vaderSentiment not installed\")\n    vaderSentiment = VaderSentimentImportMock()\n\n\nclass TextAnalysis:\n    IMAGE_ENDINGS = [\"png\", \"jpg\", \"jpeg\", \"gif\", \"jfif\", \"tiff\", \"bmp\", \"ppm\", \"pgm\", \"pbm\", \"pnm\", \"webp\", \"hdr\",\n                     \"heif\",\n                     \"bat\", \"bpg\", \"svg\", \"cgm\"]\n\n    def __init__(self):\n        super().__init__()\n        self.analyzer = vaderSentiment.SentimentIntensityAnalyzer()\n        # self.test()\n\n    def analyse(self,  text):\n        # The compound score is computed by summing the valence scores of each word in the lexicon, adjusted according\n        # to the rules, and then normalized to be between -1 (most extreme negative) and +1 (most extreme positive).\n        # https://github.com/cjhutto/vaderSentiment\n        return self.analyzer.polarity_scores(text)[\"compound\"]\n\n    # return a list of high influential value websites\n    @staticmethod\n    def get_high_value_websites():\n        return [\n            \"https://www.youtube.com\"\n        ]\n\n    @staticmethod\n    def is_analysable_url(url):\n        url_ending = str(url).split(\".\")[-1]\n        return url_ending.lower() not in TextAnalysis.IMAGE_ENDINGS\n\n    # official account tweets that can be used for testing purposes\n    def test(self):\n        texts = [\n            \"Have you read about VeChain and INPI ASIA's integration to bring nanotechnology for digital identity to \"\n            \"the VeChainThor blockchain? NDCodes resist high temperature, last over 100 years, are incredibly durable \"\n            \"and invisible to the naked eye\",\n            \"A scientific hypothesis about how cats, infected with toxoplasmosis, are making humans buy Bitcoin was \"\n            \"presented at last night's BAHFest at MIT.\",\n            \"Net Neutrality Ends! Substratum Update 4.23.18\",\n            \"One more test from @SubstratumNet for today. :)\",\n            \"Goldman Sachs hires crypto trader as head of digital assets markets\",\n            \"Big news coming! Scheduled to be 27th/28th April... Have a guess...\",\n            \"This week's Theta Surge on http://SLIVER.tv  isn't just for virtual items... five PlayStation 4s will \"\n            \"be given out to viewers that use Theta Tokens to reward the featured #Fortnite streamer! Tune in this \"\n            \"Friday at 1pm PST to win!\",\n            \"The European Parliament has voted for regulations to prevent the use of cryptocurrencies in money \"\n            \"laundering and terrorism financing. As long as they have good intention i don' t care.. but how \"\n            \"much can we trust them??!?!\"\n            \"By partnering with INPI ASIA, the VeChainThor Platform incorporates nanotechnology with digital \"\n            \"identification to provide solutions to some of the worlds most complex IoT problems.\",\n            \"Thanks to the China Academy of Information and Communication Technology, IPRdaily and Nashwork for \"\n            \"organizing the event.\",\n            \"Delivered a two hour open course last week in Beijing. You can tell the awareness of blockchain is \"\n            \"drastically increasing by the questions asked by the audience. But people need hand holding and \"\n            \"business friendly features to adopt the tech.\",\n            \"Introducing the first Oracle Enabler tool of the VeChainThor Platform: Multi-Party Payment Protocol \"\n            \"(MPP).\",\n            \"An open letter from Sunny Lu (CEO) on VeChainThor Platform.\",\n            \"VeChain has finished the production of digital intellectual property services with partner iTaotaoke. \"\n            \"This solution provides a competitive advantage for an industry in need of trust-free reporting and \"\n            \"content protections.#GoVeChain\",\n            \"Special thanks to @GaboritMickael to have invited @vechainofficial to present our solution and make \"\n            \"a little demo to @AccentureFrance\",\n            \"VeChain will pitch their solutions potentially landing a co-development product with LVMH.  In \"\n            \"attendance will be CEOs Bill McDermott (SAP), Chuck Robbins (CISCO), Ginni Rometty (IBM), and Stephane \"\n            \"Richard (Orange) as speakers -\",\n            \"As the only blockchain company selected, VeChain is among 30 of 800+ hand-picked startups to compete \"\n            \"for the second edition of the LVMH Innovation Award. As a result, VeChain has been invited to join the \"\n            \"Luxury Lab LVMH at Viva Technology in Paris from May 24-26, 2018.\",\n            \"VeChain to further its partnership with RFID leader Xiamen Innov and newly announced top enterprise \"\n            \"solution provider CoreLink by deploying a VeChainThor enterprise level decentralized application - \"\n            \"AssetLink.\",\n            \"Today, a group of senior leaders from TCL's Eagle Talent program visited the VeChain SH office. \"\n            \"@VeChain_GU demonstrated our advanced enterprise solutions and it's relation to TCL's market. As a \"\n            \"result, we're exploring new developments within TCL related to blockchain technology.\",\n            \"We are glad to be recognized as Top 10 blockchain technology solution providers in 2018. outprovides a \"\n            \"platform for CIOs and decision makers to share their experiences, wisdom and advice. Read the full \"\n            \"version article via\",\n            \"Talked about TOTO at the blockchain seminar in R University of Science and Technology business school \"\n            \"last Saturday. It covered 3000 MBA students across business schools in China.\"\n        ]\n        for text in texts:\n            print(str(self.analyse(text)) + \" => \"+str(text.encode(\"utf-8\", \"ignore\")))\n"
  },
  {
    "path": "Evaluator/Util/trend_analysis/__init__.py",
    "content": "from .trend_analysis import TrendAnalysis"
  },
  {
    "path": "Evaluator/Util/trend_analysis/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TrendAnalysis\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Evaluator/Util/trend_analysis/trend_analysis.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport numpy as np\n\n\nclass TrendAnalysis:\n\n    # trend < 0 --> Down trend\n    # trend > 0 --> Up trend\n    @staticmethod\n    def get_trend(data, averages_to_use):\n        trend = 0\n        inc = round(1 / len(averages_to_use), 2)\n        averages = []\n\n        # Get averages\n        for average_to_use in averages_to_use:\n            data_to_mean = data[-average_to_use:]\n            if len(data_to_mean):\n                averages.append(np.mean(data_to_mean))\n            else:\n                averages.append(0)\n\n        for a in range(0, len(averages) - 1):\n            if averages[a] - averages[a + 1] > 0:\n                trend -= inc\n            else:\n                trend += inc\n\n        return trend\n\n    @staticmethod\n    def peak_has_been_reached_already(data, neutral_val=0):\n        if len(data) > 1:\n            min_val = min(data)\n            max_val = max(data)\n            current_val = data[-1] / 0.8\n            if current_val > neutral_val:\n                return current_val < max_val\n            else:\n                return current_val > min_val\n        else:\n            return False\n\n    @staticmethod\n    def min_has_just_been_reached(data, acceptance_window=0.8, delay=1):\n        if len(data) > 1:\n            min_val = min(data)\n            current_val = data[-1] / acceptance_window\n            accepted_delayed_min = data[-(delay+1):]\n            return bool(min_val in accepted_delayed_min and current_val > min_val)\n        else:\n            return False\n\n    @staticmethod\n    # TODO\n    def detect_divergence(data_frame, indicator_data_frame):\n        pass\n        # candle_data = data_frame.tail(DIVERGENCE_USED_VALUE)\n        # indicator_data = indicator_data_frame.tail(DIVERGENCE_USED_VALUE)\n        #\n        # total_delta = []\n        #\n        # for i in range(0, DIVERGENCE_USED_VALUE - 1):\n        #     candle_delta = candle_data.values[i] - candle_data.values[i + 1]\n        #     indicator_delta = indicator_data.values[i] - indicator_data.values[i + 1]\n        #     total_delta.append(candle_delta - indicator_delta)\n\n    @staticmethod\n    def get_estimation_of_move_state_relatively_to_previous_moves_length(mean_crossing_indexes,\n                                                                         current_trend,\n                                                                         pattern_move_size=1,\n                                                                         double_size_patterns_count=0):\n\n        if mean_crossing_indexes:\n            # compute average move size\n            time_averages = [(lambda a: mean_crossing_indexes[a+1]-mean_crossing_indexes[a])(a)\n                             for a in range(len(mean_crossing_indexes)-1)]\n            # add 1st length\n            if 0 != mean_crossing_indexes[0]:\n                time_averages.append(mean_crossing_indexes[0])\n\n            # take double_size_patterns_count into account\n            time_averages += [0]*double_size_patterns_count\n\n            time_average = np.mean(time_averages)*pattern_move_size if time_averages else 0\n\n            current_move_length = len(current_trend) - mean_crossing_indexes[-1]\n            # higher than time_average => high chances to be at half of the move already\n            if current_move_length > time_average/2:\n                return 1\n            else:\n                return current_move_length / (time_average/2)\n        else:\n            return 0\n\n    @staticmethod\n    def get_threshold_change_indexes(data, threshold):\n\n        # sub threshold values\n        sub_threshold_indexes = np.where(data <= threshold)[0]\n\n        # remove consecutive sub-threshold values because they are not crosses\n        threshold_crossing_indexes = []\n        current_move_size = 1\n        for i, index in enumerate(sub_threshold_indexes):\n            if not len(threshold_crossing_indexes):\n                threshold_crossing_indexes.append(index)\n            else:\n                if threshold_crossing_indexes[-1] == index - current_move_size:\n                    current_move_size += 1\n                else:\n                    if sub_threshold_indexes[i-1] not in threshold_crossing_indexes:\n                        threshold_crossing_indexes.append(sub_threshold_indexes[i-1])\n                    if index not in threshold_crossing_indexes:\n                        threshold_crossing_indexes.append(index)\n                    current_move_size = 1\n        # add last index if data_frame ends above threshold and last threshold_crossing_indexes inferior\n        # to data_frame size\n        if len(sub_threshold_indexes) > 0 \\\n                and sub_threshold_indexes[-1] < len(data) \\\n                and data[-1] > threshold \\\n                and sub_threshold_indexes[-1]+1 not in threshold_crossing_indexes:\n            threshold_crossing_indexes.append(sub_threshold_indexes[-1]+1)\n\n        return threshold_crossing_indexes\n\n    @staticmethod\n    def have_just_crossed_over(list_1, list_2):\n        # returns True if the last value of list_1 is higher than the last value of list_2 but the immediately\n        # preceding list_1 value is lower than the one from list_2\n        try:\n            return list_1[-1] > list_2[-1] and list_1[-2] < list_2[-2]\n        except KeyError:\n            return False\n"
  },
  {
    "path": "LICENSE",
    "content": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n\n  This version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n  0. Additional Definitions.\n\n  As used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n  \"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\n  An \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\n  A \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library.  The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\n  The \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\n  The \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n  1. Exception to Section 3 of the GNU GPL.\n\n  You may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n  2. Conveying Modified Versions.\n\n  If you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\n   a) under this License, provided that you make a good faith effort to\n   ensure that, in the event an Application does not supply the\n   function or data, the facility still operates, and performs\n   whatever part of its purpose remains meaningful, or\n\n   b) under the GNU GPL, with none of the additional permissions of\n   this License applicable to that copy.\n\n  3. Object Code Incorporating Material from Library Header Files.\n\n  The object code form of an Application may incorporate material from\na header file that is part of the Library.  You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\n   a) Give prominent notice with each copy of the object code that the\n   Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the object code with a copy of the GNU GPL and this license\n   document.\n\n  4. Combined Works.\n\n  You may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\n   a) Give prominent notice with each copy of the Combined Work that\n   the Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the Combined Work with a copy of the GNU GPL and this license\n   document.\n\n   c) For a Combined Work that displays copyright notices during\n   execution, include the copyright notice for the Library among\n   these notices, as well as a reference directing the user to the\n   copies of the GNU GPL and this license document.\n\n   d) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\n   e) Provide Installation Information, but only if you would otherwise\n   be required to provide such information under section 6 of the\n   GNU GPL, and only to the extent that such information is\n   necessary to install and execute a modified version of the\n   Combined Work produced by recombining or relinking the\n   Application with a modified version of the Linked Version. (If\n   you use option 4d0, the Installation Information must accompany\n   the Minimal Corresponding Source and Corresponding Application\n   Code. If you use option 4d1, you must provide the Installation\n   Information in the manner specified by section 6 of the GNU GPL\n   for conveying Corresponding Source.)\n\n  5. Combined Libraries.\n\n  You may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\n   a) Accompany the combined library with a copy of the same work based\n   on the Library, uncombined with any other library facilities,\n   conveyed under the terms of this License.\n\n   b) Give prominent notice with the combined library that part of it\n   is a work based on the Library, and explaining where to find the\n   accompanying uncombined form of the same work.\n\n  6. Revised Versions of the GNU Lesser General Public License.\n\n  The Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\n  Each version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\n  If the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/__init__.py",
    "content": "# pylint: disable=R0801\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators\nfrom tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators import (\n    OHLCVOperator,\n    ExchangeDataDependency,\n    create_ohlcv_operators,\n)\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators\nfrom tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators import (\n    PortfolioOperator,\n    create_portfolio_operators,\n)\n\n\n__all__ = [\n    \"OHLCVOperator\",\n    \"ExchangeDataDependency\",\n    \"create_ohlcv_operators\",\n    \"PortfolioOperator\",\n    \"create_portfolio_operators\",\n]\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/exchange_operator.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges\n\nimport octobot_commons.dsl_interpreter.operators.call_operator as dsl_interpreter_call_operator\nimport octobot_trading.modes.script_keywords as script_keywords\n\n\nEXCHANGE_LIBRARY = \"exchange\"\nUNINITIALIZED_VALUE = object()\n\n\nclass ExchangeOperator(dsl_interpreter_call_operator.CallOperator):\n\n    @staticmethod\n    def get_library() -> str:\n        \"\"\"\n        Get the library of the operator.\n        \"\"\"\n        return EXCHANGE_LIBRARY\n\n    async def get_context(\n        self, exchange_manager: octobot_trading.exchanges.ExchangeManager\n    ) -> script_keywords.Context:\n        # todo later: handle exchange manager without initialized trading modes\n        return script_keywords.get_base_context(next(iter(exchange_manager.trading_modes)))\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/exchange_private_data_operators/__init__.py",
    "content": "# pylint: disable=R0801\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators.portfolio_operators\nfrom tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators.portfolio_operators import (\n    PortfolioOperator,\n    create_portfolio_operators,\n)\n__all__ = [\n    \"PortfolioOperator\",\n    \"create_portfolio_operators\",\n]"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/exchange_private_data_operators/portfolio_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_commons.constants\nimport octobot_commons.errors\nimport octobot_commons.dsl_interpreter as dsl_interpreter\nimport octobot_trading.personal_data\nimport octobot_trading.exchanges\nimport octobot_trading.api\n\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_operator as exchange_operator\n\n\nclass PortfolioOperator(exchange_operator.ExchangeOperator):\n    def __init__(self, *parameters: dsl_interpreter.OperatorParameterType, **kwargs: typing.Any):\n        super().__init__(*parameters, **kwargs)\n        self.value: dsl_interpreter_operator.ComputedOperatorParameterType = exchange_operator.UNINITIALIZED_VALUE # type: ignore\n\n    @staticmethod\n    def get_library() -> str:\n        # this is a contextual operator, so it should not be included by default in the get_all_operators function return values\n        return octobot_commons.constants.CONTEXTUAL_OPERATORS_LIBRARY\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"asset\", description=\"the asset to get the value for\", required=False, type=str),\n        ]\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        if self.value is exchange_operator.UNINITIALIZED_VALUE:\n            raise octobot_commons.errors.DSLInterpreterError(\"{self.__class__.__name__} has not been initialized\")\n        return self.value\n\n\ndef create_portfolio_operators(\n    exchange_manager: typing.Optional[octobot_trading.exchanges.ExchangeManager],\n) -> typing.List[type[PortfolioOperator]]:\n\n    def _get_asset_holdings(asset: str) -> octobot_trading.personal_data.Asset:\n        return octobot_trading.api.get_portfolio_currency(exchange_manager, asset)\n\n    class _TotalOperator(PortfolioOperator):\n        DESCRIPTION = \"Returns the total holdings of the asset in the portfolio\"\n        EXAMPLE = \"total('BTC')\"\n\n        @staticmethod\n        def get_name() -> str:\n            return \"total\"\n\n        async def pre_compute(self) -> None:\n            await super().pre_compute()\n            asset = self.get_computed_parameters()[0]\n            self.value = float(_get_asset_holdings(asset).total)\n\n    class _AvailableOperator(PortfolioOperator):\n        DESCRIPTION = \"Returns the available holdings of the asset in the portfolio\"\n        EXAMPLE = \"available('BTC')\"\n\n        @staticmethod\n        def get_name() -> str:\n            return \"available\"\n\n        async def pre_compute(self) -> None:\n            await super().pre_compute()\n            asset = self.get_computed_parameters()[0]\n            self.value = float(_get_asset_holdings(asset).available)\n        \n\n    return [_TotalOperator, _AvailableOperator]\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/exchange_public_data_operators/__init__.py",
    "content": "# pylint: disable=R0801\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.ohlcv_operators\nfrom tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.ohlcv_operators import (\n    OHLCVOperator,\n    ExchangeDataDependency,\n    create_ohlcv_operators,\n)\n\n__all__ = [\n    \"OHLCVOperator\",\n    \"ExchangeDataDependency\",\n    \"create_ohlcv_operators\",\n]"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/exchange_public_data_operators/ohlcv_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\nimport dataclasses\nimport numpy as np\n\nimport octobot_commons.constants\nimport octobot_commons.errors\nimport octobot_commons.logging\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.dsl_interpreter as dsl_interpreter\nimport octobot_trading.exchanges\nimport octobot_trading.exchange_data\nimport octobot_trading.api\nimport octobot_trading.constants\n\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_operator as exchange_operator\n\n\n@dataclasses.dataclass\nclass ExchangeDataDependency(dsl_interpreter.InterpreterDependency):\n    exchange_manager_id: str\n    symbol: typing.Optional[str]\n    time_frame: typing.Optional[str]\n    data_source: str = octobot_trading.constants.OHLCV_CHANNEL\n\n    def __hash__(self) -> int:\n        return hash((self.exchange_manager_id, self.symbol, self.time_frame, self.data_source))\n\n\nclass OHLCVOperator(exchange_operator.ExchangeOperator):\n    def __init__(self, *parameters: dsl_interpreter.OperatorParameterType, **kwargs: typing.Any):\n        super().__init__(*parameters, **kwargs)\n        self.value: dsl_interpreter_operator.ComputedOperatorParameterType = exchange_operator.UNINITIALIZED_VALUE # type: ignore\n\n    @staticmethod\n    def get_library() -> str:\n        # this is a contextual operator, so it should not be included by default in the get_all_operators function return values\n        return octobot_commons.constants.CONTEXTUAL_OPERATORS_LIBRARY\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"symbol\", description=\"the symbol to get the OHLCV data for\", required=False, type=str),\n            dsl_interpreter.OperatorParameter(name=\"time_frame\", description=\"the time frame to get the OHLCV data for\", required=False, type=str),\n        ]\n\n    def get_symbol_and_time_frame(self) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:\n        if parameters := self.get_computed_parameters():\n            symbol = parameters[0] if len(parameters) > 0 else None\n            time_frame = parameters[1] if len(parameters) > 1 else None\n            return (\n                str(symbol) if symbol is not None else None,\n                str(time_frame) if time_frame is not None else None\n            )\n        return None, None\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        if self.value is exchange_operator.UNINITIALIZED_VALUE:\n            raise octobot_commons.errors.DSLInterpreterError(\"{self.__class__.__name__} has not been initialized\")\n        return self.value\n\n\ndef create_ohlcv_operators(\n    exchange_manager: typing.Optional[octobot_trading.exchanges.ExchangeManager],\n    symbol: typing.Optional[str],\n    time_frame: typing.Optional[str],\n    candle_manager_by_time_frame_by_symbol: typing.Optional[\n        typing.Dict[str, typing.Dict[str, octobot_trading.exchange_data.CandlesManager]]\n    ] = None\n) -> typing.List[type[OHLCVOperator]]:\n\n    if exchange_manager is None and candle_manager_by_time_frame_by_symbol is None:\n        raise octobot_commons.errors.InvalidParametersError(\"exchange_manager or candle_manager_by_time_frame_by_symbol must be provided\")\n\n    def _get_candles_values_with_latest_kline_if_available(\n        input_symbol: typing.Optional[str], input_time_frame: typing.Optional[str],\n        value_type: commons_enums.PriceIndexes, limit: int = -1\n    ) -> np.ndarray:\n        _symbol = input_symbol or symbol\n        _time_frame = input_time_frame or time_frame\n        if exchange_manager is None:\n            if candle_manager_by_time_frame_by_symbol is not None:\n                candles_manager = candle_manager_by_time_frame_by_symbol[_time_frame][_symbol]\n            symbol_data = None\n        else:\n            symbol_data = octobot_trading.api.get_symbol_data(\n                exchange_manager, _symbol, allow_creation=False\n            )\n            candles_manager = octobot_trading.api.get_symbol_candles_manager(\n                symbol_data, _time_frame\n            )\n        candles_values = _get_candles_values(candles_manager, value_type, limit)\n        if symbol_data is not None and (kline := _get_kline(symbol_data, _time_frame)):\n            kline_time = kline[commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n            last_candle_time = candles_manager.time_candles[candles_manager.time_candles_index - 1]\n            if kline_time == last_candle_time:\n                # kline is an update of the last candle\n                return _adapt_last_candle_value(candles_manager, value_type, candles_values, kline)\n            else:\n                tf_seconds = commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(_time_frame)] * octobot_commons.constants.MINUTE_TO_SECONDS\n                if kline_time == last_candle_time + tf_seconds:\n                    # kline is a new candle\n                    kline_value = kline[value_type.value]\n                    return np.append(candles_values[1:], kline_value)\n                else:\n                    octobot_commons.logging.get_logger(OHLCVOperator.__name__).error(\n                        f\"{exchange_manager.exchange_name + '' if exchange_manager is not None else ''}{_symbol} {_time_frame} \"\n                        f\"kline time ({kline_time}) is not equal to last candle time not the last time + {_time_frame} \"\n                        f\"({last_candle_time} + {tf_seconds}) seconds. Kline has been ignored.\"\n                    )\n        return candles_values\n\n    def _get_dependencies() -> typing.List[ExchangeDataDependency]:\n        return [\n            ExchangeDataDependency(\n                exchange_manager_id=octobot_trading.api.get_exchange_manager_id(exchange_manager),\n                symbol=symbol,\n                time_frame=time_frame\n            )\n        ]\n\n    class _LocalOHLCVOperator(OHLCVOperator):\n        PRICE_INDEX: commons_enums.PriceIndexes = None # type: ignore\n\n        def get_dependencies(self) -> typing.List[dsl_interpreter.InterpreterDependency]:\n            return super().get_dependencies() + _get_dependencies()\n\n        async def pre_compute(self) -> None:\n            await super().pre_compute()\n            self.value = _get_candles_values_with_latest_kline_if_available(*self.get_symbol_and_time_frame(), self.PRICE_INDEX, -1)\n    \n    class _OpenPriceOperator(_LocalOHLCVOperator):\n        DESCRIPTION = \"Returns the candle's open price as array of floats\"\n        EXAMPLE = \"open('BTC/USDT', '1h')\"\n\n        PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_OPEN\n\n        @staticmethod\n        def get_name() -> str:\n            return \"open\"\n    \n    class _HighPriceOperator(_LocalOHLCVOperator):\n        DESCRIPTION = \"Returns the candle's high price as array of floats\"\n        EXAMPLE = \"high('BTC/USDT', '1h')\"\n\n        PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_HIGH\n\n        @staticmethod\n        def get_name() -> str:\n            return \"high\"\n\n    class _LowPriceOperator(_LocalOHLCVOperator):\n        DESCRIPTION = \"Returns the candle's low price as array of floats\"\n        EXAMPLE = \"low('BTC/USDT', '1h')\"\n\n        PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_LOW\n\n        @staticmethod\n        def get_name() -> str:\n            return \"low\"\n\n    class _ClosePriceOperator(_LocalOHLCVOperator):\n        DESCRIPTION = \"Returns the candle's close price as array of floats\"\n        EXAMPLE = \"close('BTC/USDT', '1h')\"\n\n        PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_CLOSE\n\n        @staticmethod\n        def get_name() -> str:\n            return \"close\"\n\n    class _VolumePriceOperator(_LocalOHLCVOperator):\n        DESCRIPTION = \"Returns the candle's volume as array of floats\"\n        EXAMPLE = \"volume('BTC/USDT', '1h')\"\n\n        PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_VOL\n\n        @staticmethod\n        def get_name() -> str:\n            return \"volume\"\n    \n    class _TimePriceOperator(_LocalOHLCVOperator):\n        DESCRIPTION = \"Returns the candle's time as array of floats\"\n        EXAMPLE = \"time('BTC/USDT', '1h')\"\n\n        PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_TIME\n\n        @staticmethod\n        def get_name() -> str:\n            return \"time\"\n\n    return [_OpenPriceOperator, _HighPriceOperator, _LowPriceOperator, _ClosePriceOperator, _VolumePriceOperator, _TimePriceOperator]\n\ndef _get_kline(\n    symbol_data: octobot_trading.exchange_data.ExchangeSymbolData, _time_frame: str\n) -> typing.Optional[list]:\n    try:\n        return octobot_trading.api.get_symbol_klines(symbol_data, _time_frame)\n    except KeyError:\n        return None\n\n\ndef _get_candles_values(\n    candles_manager: octobot_trading.exchange_data.CandlesManager,\n    candle_value: commons_enums.PriceIndexes, limit: int = -1\n) -> np.ndarray:\n    match candle_value:\n        case commons_enums.PriceIndexes.IND_PRICE_CLOSE:\n            return candles_manager.get_symbol_close_candles(limit)\n        case commons_enums.PriceIndexes.IND_PRICE_OPEN:\n            return candles_manager.get_symbol_open_candles(limit)\n        case commons_enums.PriceIndexes.IND_PRICE_HIGH:\n            return candles_manager.get_symbol_high_candles(limit)\n        case commons_enums.PriceIndexes.IND_PRICE_LOW:\n            return candles_manager.get_symbol_low_candles(limit)\n        case commons_enums.PriceIndexes.IND_PRICE_VOL:\n            return candles_manager.get_symbol_volume_candles(limit)\n        case commons_enums.PriceIndexes.IND_PRICE_TIME:\n            return candles_manager.get_symbol_time_candles(limit)\n        case _:\n            raise octobot_commons.errors.InvalidParametersError(f\"Invalid candle value: {candle_value}\")\n\ndef _adapt_last_candle_value(\n    candles_manager: octobot_trading.exchange_data.CandlesManager,\n    candle_value: commons_enums.PriceIndexes,\n    candles_values: np.ndarray,\n    kline: list\n) -> np.ndarray:\n    match candle_value:\n        case commons_enums.PriceIndexes.IND_PRICE_CLOSE:\n            candles_values[candles_manager.close_candles_index - 1] = kline[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value]\n        case commons_enums.PriceIndexes.IND_PRICE_OPEN:\n            candles_values[candles_manager.open_candles_index - 1] = kline[commons_enums.PriceIndexes.IND_PRICE_OPEN.value]\n        case commons_enums.PriceIndexes.IND_PRICE_HIGH:\n            candles_values[candles_manager.high_candles_index - 1] = kline[commons_enums.PriceIndexes.IND_PRICE_HIGH.value]\n        case commons_enums.PriceIndexes.IND_PRICE_LOW:\n            candles_values[candles_manager.low_candles_index - 1] = kline[commons_enums.PriceIndexes.IND_PRICE_LOW.value]\n        case commons_enums.PriceIndexes.IND_PRICE_VOL:\n            candles_values[candles_manager.volume_candles_index - 1] = kline[commons_enums.PriceIndexes.IND_PRICE_VOL.value]\n        case commons_enums.PriceIndexes.IND_PRICE_TIME:\n            # nothing to do for time (this value is constant)\n            pass\n        case _:\n            raise octobot_commons.errors.InvalidParametersError(f\"Invalid candle value: {candle_value}\")\n    return candles_values\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport mock\nimport pytest\nimport typing\n\nimport numpy as np\n\nimport octobot_commons.enums\nimport octobot_commons.errors\nimport octobot_commons.constants\nimport octobot_commons.dsl_interpreter as dsl_interpreter\nimport tentacles.Meta.DSL_operators.exchange_operators as exchange_operators\n\n\nSYMBOL = \"BTC/USDT\"\nSYMBOL2 = \"ETH/USDT\"\nTIME_FRAME = \"1h\"\nTIME_FRAME2 = \"4h\"\nKLINE_SIGNATURE = 0.00666\n\n\n@pytest.fixture\ndef historical_prices():\n    return np.array([\n        81.59, 81.06, 82.87, 83, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36, 85.53, 86.54, 86.89, \n        87.77, 87.29, 87.18, 87.01, 89.02, 89.68, 90.36, 92.83, 93.37, 93.02, 93.45, 94.13, \n        93.12, 93.18, 92.08, 92.82, 92.92, 92.25, 92.22\n    ])\n\n@pytest.fixture\ndef historical_times(historical_prices):\n    return np.array([\n        i + 10 for i in range(len(historical_prices))\n    ], dtype=np.float64)\n\n\n@pytest.fixture\ndef historical_volume(historical_prices):\n    base_volume_pattern = [\n        # will create an int np.array, which will updated to float64 to comply with tulipy requirements\n        903, 1000, 2342, 992, 900, 1231, 1211, 1113\n    ]\n    return np.array(base_volume_pattern*(len(historical_prices) // len(base_volume_pattern) + 1), dtype=np.float64)[:len(historical_prices)]\n\n\ndef _get_candle_managers(historical_prices, historical_volume, historical_times):\n    btc_1h_candles_manager = mock.Mock(\n        get_symbol_open_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy()),\n        get_symbol_high_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy()),\n        get_symbol_low_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy()),\n        get_symbol_close_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy()),\n        get_symbol_volume_candles=mock.Mock(side_effect=lambda _ : historical_volume.copy()),\n        get_symbol_time_candles=mock.Mock(side_effect=lambda _ : historical_times.copy()),\n        time_candles_index=len(historical_times),\n        open_candles_index=len(historical_prices),\n        high_candles_index=len(historical_prices),\n        low_candles_index=len(historical_prices),\n        close_candles_index=len(historical_prices),\n        volume_candles_index=len(historical_volume),\n        time_candles=historical_times,\n    )\n    eth_1h_candles_manager = mock.Mock(\n        get_symbol_open_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() / 2),\n        get_symbol_high_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() / 2),\n        get_symbol_low_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() / 2),\n        get_symbol_close_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() / 2),\n        get_symbol_volume_candles=mock.Mock(side_effect=lambda _ : historical_volume.copy() / 2),\n        get_symbol_time_candles=mock.Mock(side_effect=lambda _ : historical_times.copy() / 2),\n        time_candles_index=len(historical_times),\n        open_candles_index=len(historical_prices),\n        high_candles_index=len(historical_prices),\n        low_candles_index=len(historical_prices),\n        close_candles_index=len(historical_prices),\n        volume_candles_index=len(historical_volume),\n        time_candles=historical_times / 2,\n    )\n    btc_4h_candles_manager = mock.Mock(\n        get_symbol_open_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() * 2),\n        get_symbol_high_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() * 2),\n        get_symbol_low_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() * 2),\n        get_symbol_close_candles=mock.Mock(side_effect=lambda _ : historical_prices.copy() * 2),\n        get_symbol_volume_candles=mock.Mock(side_effect=lambda _ : historical_volume.copy() * 2),\n        get_symbol_time_candles=mock.Mock(side_effect=lambda _ : historical_times.copy() * 2),\n        time_candles_index=len(historical_times),\n        open_candles_index=len(historical_prices),\n        high_candles_index=len(historical_prices),\n        low_candles_index=len(historical_prices),\n        close_candles_index=len(historical_prices),\n        volume_candles_index=len(historical_volume),\n        time_candles=historical_times * 2,\n    )\n    return (\n        btc_1h_candles_manager,\n        eth_1h_candles_manager,\n        btc_4h_candles_manager,\n    )\n\n\ndef _get_kline(candles_manager: mock.Mock, signature: float, kline_time_delta: typing.Optional[float]) -> list:\n    kline = [0] * len(octobot_commons.enums.PriceIndexes)\n    kline[octobot_commons.enums.PriceIndexes.IND_PRICE_TIME.value] = (\n        candles_manager.get_symbol_time_candles(-1)[-1] + kline_time_delta\n        if kline_time_delta is not None\n        else candles_manager.get_symbol_time_candles(-1)[-1]\n    )\n    kline[octobot_commons.enums.PriceIndexes.IND_PRICE_OPEN.value] = candles_manager.get_symbol_open_candles(-1)[-1] + signature\n    kline[octobot_commons.enums.PriceIndexes.IND_PRICE_HIGH.value] = candles_manager.get_symbol_high_candles(-1)[-1] + signature\n    kline[octobot_commons.enums.PriceIndexes.IND_PRICE_LOW.value] = candles_manager.get_symbol_low_candles(-1)[-1] + signature\n    kline[octobot_commons.enums.PriceIndexes.IND_PRICE_CLOSE.value] = candles_manager.get_symbol_close_candles(-1)[-1] + signature\n    kline[octobot_commons.enums.PriceIndexes.IND_PRICE_VOL.value] = candles_manager.get_symbol_volume_candles(-1)[-1] + signature\n    return kline\n\n\ndef _get_symbol_data_factory(\n    btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, kline_type: str\n):\n    def _get_symbol_data(symbol: str, **kwargs):\n        symbol_candles = {}\n        one_h_candles_manager = btc_1h_candles_manager if symbol == SYMBOL else eth_1h_candles_manager if symbol == SYMBOL2 else None\n        four_h_candles_manager = btc_4h_candles_manager if symbol == SYMBOL else None # no 4h eth candles\n        if one_h_candles_manager is None and four_h_candles_manager is None:\n            raise octobot_commons.errors.InvalidParametersError(f\"Symbol {symbol} not found\")\n        symbol_candles[octobot_commons.enums.TimeFrames(TIME_FRAME)] = one_h_candles_manager\n        if four_h_candles_manager:\n            symbol_candles[octobot_commons.enums.TimeFrames(TIME_FRAME2)] = four_h_candles_manager\n        if kline_type == \"no_kline\":\n            symbol_klines = {}\n        elif kline_type == \"same_time_kline\":\n            symbol_klines = {\n                octobot_commons.enums.TimeFrames(TIME_FRAME): mock.Mock(kline=_get_kline(one_h_candles_manager, KLINE_SIGNATURE, None)),\n            }\n            if four_h_candles_manager:\n                symbol_klines[octobot_commons.enums.TimeFrames(TIME_FRAME2)] = mock.Mock(kline=_get_kline(four_h_candles_manager, KLINE_SIGNATURE, None))\n        elif kline_type == \"new_time_kline\":\n            symbol_klines = {\n                octobot_commons.enums.TimeFrames(TIME_FRAME): mock.Mock(kline=_get_kline(\n                    one_h_candles_manager, KLINE_SIGNATURE, \n                    octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames(TIME_FRAME)] * octobot_commons.constants.MINUTE_TO_SECONDS\n                )),\n            }\n            if four_h_candles_manager:\n                symbol_klines[octobot_commons.enums.TimeFrames(TIME_FRAME2)] = mock.Mock(kline=_get_kline(\n                    four_h_candles_manager, KLINE_SIGNATURE, \n                    octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames(TIME_FRAME2)] * octobot_commons.constants.MINUTE_TO_SECONDS\n                ))\n        else:\n            raise NotImplementedError(f\"Kline type {kline_type} not implemented\")\n        return mock.Mock(\n            symbol_candles=symbol_candles,\n            symbol_klines=symbol_klines\n        )\n    return _get_symbol_data\n\n\n@pytest.fixture\ndef exchange_manager_with_candles(historical_prices, historical_volume, historical_times):\n    btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager = _get_candle_managers(\n        historical_prices, historical_volume, historical_times\n    )\n    return mock.Mock(\n        id=\"exchange_manager_id\",\n        exchange_name=\"binance\",\n        exchange_symbols_data=mock.Mock(\n            get_exchange_symbol_data=_get_symbol_data_factory(\n                btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, \"no_kline\"\n            )\n        )\n    )\n\n\n@pytest.fixture\ndef exchange_manager_with_candles_and_klines(historical_prices, historical_volume, historical_times):\n    btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager = _get_candle_managers(\n        historical_prices, historical_volume, historical_times\n    )\n    return mock.Mock(\n        id=\"exchange_manager_id\",\n        exchange_name=\"binance\",\n        exchange_symbols_data=mock.Mock(\n            get_exchange_symbol_data=_get_symbol_data_factory(\n                btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, \"same_time_kline\"\n            )\n        )\n    )\n\n\n@pytest.fixture\ndef exchange_manager_with_candles_and_new_candle_klines(historical_prices, historical_volume, historical_times):\n    btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager = _get_candle_managers(\n        historical_prices, historical_volume, historical_times\n    )\n    return mock.Mock(\n        id=\"exchange_manager_id\",\n        exchange_name=\"binance\",\n        exchange_symbols_data=mock.Mock(\n            get_exchange_symbol_data=_get_symbol_data_factory(\n                btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, \"new_time_kline\"\n            )\n        )\n    )\n\n\n@pytest.fixture\ndef candle_manager_by_time_frame_by_symbol(historical_prices, historical_volume, historical_times):\n    btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager = _get_candle_managers(\n        historical_prices, historical_volume, historical_times\n    )\n    return {\n        TIME_FRAME: {\n            SYMBOL: btc_1h_candles_manager,\n            SYMBOL2: eth_1h_candles_manager,\n        },\n        TIME_FRAME2: {\n            SYMBOL: btc_4h_candles_manager,\n        },\n    }\n\n\n@pytest.fixture\ndef interpreter(exchange_manager_with_candles):\n    return dsl_interpreter.Interpreter(\n        dsl_interpreter.get_all_operators() + \n        exchange_operators.create_ohlcv_operators(exchange_manager_with_candles, SYMBOL, TIME_FRAME)\n    )\n\n\n@pytest.fixture\ndef interpreter_with_exchange_manager_and_klines(exchange_manager_with_candles_and_klines):\n    return dsl_interpreter.Interpreter(\n        dsl_interpreter.get_all_operators() + \n        exchange_operators.create_ohlcv_operators(exchange_manager_with_candles_and_klines, SYMBOL, TIME_FRAME)\n    )\n\n\n@pytest.fixture\ndef interpreter_with_exchange_manager_and_new_candle_klines(exchange_manager_with_candles_and_new_candle_klines):\n    return dsl_interpreter.Interpreter(\n        dsl_interpreter.get_all_operators() + \n        exchange_operators.create_ohlcv_operators(exchange_manager_with_candles_and_new_candle_klines, SYMBOL, TIME_FRAME)\n    )\n\n\n@pytest.fixture\ndef interpreter_with_candle_manager_by_time_frame_by_symbol(candle_manager_by_time_frame_by_symbol):\n    return dsl_interpreter.Interpreter(\n        dsl_interpreter.get_all_operators() + \n        exchange_operators.create_ohlcv_operators(None, SYMBOL, TIME_FRAME, candle_manager_by_time_frame_by_symbol)\n    )\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/tests/exchange_public_data_operators/test_ohlcv_operators.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport mock\n\nimport numpy as np\n\nimport octobot_commons.errors\nimport octobot_commons.enums\nimport octobot_commons.constants\nimport octobot_commons.logging\nimport octobot_trading.api\nimport octobot_trading.constants\nimport tentacles.Meta.DSL_operators.exchange_operators as exchange_operators\nimport tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.ohlcv_operators as ohlcv_operators\n\n\nfrom tentacles.Meta.DSL_operators.exchange_operators.tests import (\n    SYMBOL,\n    TIME_FRAME,\n    KLINE_SIGNATURE,\n    historical_prices,\n    historical_volume,\n    historical_times,\n    exchange_manager_with_candles,\n    exchange_manager_with_candles_and_klines,\n    exchange_manager_with_candles_and_new_candle_klines,\n    candle_manager_by_time_frame_by_symbol,\n    interpreter_with_candle_manager_by_time_frame_by_symbol,\n    interpreter_with_exchange_manager_and_klines,\n    interpreter_with_exchange_manager_and_new_candle_klines,\n    interpreter,\n)\n\n\n@pytest.fixture\ndef expected_values(request, historical_prices, historical_volume, historical_times):\n    select_value = request.param\n    if select_value == \"price\":\n        return historical_prices\n    elif select_value == \"volume\":\n        return historical_volume\n    elif select_value == \"time\":\n        return historical_times\n    raise octobot_commons.errors.InvalidParametersError(f\"Invalid select_value: {select_value}\")\n\n\n@pytest.fixture\ndef operator(request):\n    return request.param\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator, expected_values\", [\n    (\"open\", \"price\"),\n    (\"high\", \"price\"),\n    (\"low\", \"price\"),\n    (\"close\", \"price\"), \n    (\"volume\", \"volume\"),\n    (\"time\", \"time\")\n], indirect=True) # use indirect=True to pass fixtures as a parameter\nasync def test_ohlcv_operators_basic_calls_without_klines(\n    interpreter, interpreter_with_candle_manager_by_time_frame_by_symbol,\n    operator, expected_values\n):\n    # test with both interpreter data sources\n    for _interpreter in [interpreter, interpreter_with_candle_manager_by_time_frame_by_symbol]:\n        # no param, use context values: SYMBOL, TIME_FRAME: BTC/USDT, 1h\n        operator_value = await _interpreter.interprete(operator)\n        assert np.array_equal(operator_value, expected_values)\n        # ensure symbol parameters are used when provided\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}('ETH/USDT')\"), expected_values / 2) # 1h ETH\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT')\"), expected_values) # 1h BTC\n\n        # ensure time frame is used when provided\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}(None, '4h')\"), expected_values * 2) # 4h BTC\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}(None, '1h')\"), expected_values) # 1h BTC\n\n        # ensure symbol and time frame are used when provided\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT', '1h')\"), expected_values) # 4h BTC rsi value\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT', '4h')\"), expected_values * 2) # 4h BTC rsi value\n        assert np.array_equal(await _interpreter.interprete(f\"{operator}('ETH/USDT', '1h')\"), expected_values / 2) # 1h ETH rsi value\n        with pytest.raises(KeyError): # no 4h ETH candles\n            await _interpreter.interprete(f\"{operator}('ETH/USDT', '4h')\")\n\n\ndef _adapted_for_kline(values: np.ndarray, operator: str, time_delay: float) -> np.ndarray:\n    adapted = values.copy()\n    if time_delay > 0:\n        adapted = np.append(adapted[1:], adapted[-1] + (time_delay if operator == \"time\" else KLINE_SIGNATURE))\n    else:\n        adapted[-1] += (0 if operator == \"time\" else KLINE_SIGNATURE)\n    return adapted\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator, expected_values\", [\n    (\"open\", \"price\"),\n    (\"high\", \"price\"),\n    (\"low\", \"price\"),\n    (\"close\", \"price\"), \n    (\"volume\", \"volume\"),\n    (\"time\", \"time\")\n], indirect=True) # use indirect=True to pass fixtures as a parameter\nasync def test_ohlcv_operators_basic_calls_with_klines(\n    interpreter_with_exchange_manager_and_klines, operator, expected_values\n):\n    # test with both interpreter data sources\n    _interpreter = interpreter_with_exchange_manager_and_klines\n    # no param, use context values: SYMBOL, TIME_FRAME: BTC/USDT, 1h\n    operator_value = await _interpreter.interprete(operator)\n    kline_adapted_value = _adapted_for_kline(expected_values, operator, 0)\n    assert np.array_equal(operator_value, kline_adapted_value)\n    # ensure symbol parameters are used when provided\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('ETH/USDT')\"), _adapted_for_kline(expected_values / 2, operator, 0)) # 1h ETH\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT')\"), kline_adapted_value) # 1h BTC\n\n    # ensure time frame is used when provided\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}(None, '4h')\"), _adapted_for_kline(expected_values * 2, operator, 0)) # 4h BTC\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}(None, '1h')\"), kline_adapted_value) # 1h BTC\n\n    # ensure symbol and time frame are used when provided\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT', '1h')\"), kline_adapted_value) # 4h BTC rsi value\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT', '4h')\"), _adapted_for_kline(expected_values * 2, operator, 0)) # 4h BTC rsi value\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('ETH/USDT', '1h')\"), _adapted_for_kline(expected_values / 2, operator, 0)) # 1h ETH rsi value\n    with pytest.raises(KeyError): # no 4h ETH candles\n        await _interpreter.interprete(f\"{operator}('ETH/USDT', '4h')\")\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator, expected_values\", [\n    (\"open\", \"price\"),\n    (\"high\", \"price\"),\n    (\"low\", \"price\"),\n    (\"close\", \"price\"), \n    (\"volume\", \"volume\"),\n    (\"time\", \"time\")\n], indirect=True) # use indirect=True to pass fixtures as a parameter\nasync def test_ohlcv_operators_basic_calls_with_new_candle_klines(\n    interpreter_with_exchange_manager_and_new_candle_klines, operator, expected_values\n):\n    # test with both interpreter data sources\n    _interpreter = interpreter_with_exchange_manager_and_new_candle_klines\n    # no param, use context values: SYMBOL, TIME_FRAME: BTC/USDT, 1h\n    operator_value = await _interpreter.interprete(operator)\n    one_hour_time_delay = octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames(\"1h\")] * octobot_commons.constants.MINUTE_TO_SECONDS\n    four_hours_time_delay = octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames(\"4h\")] * octobot_commons.constants.MINUTE_TO_SECONDS\n    kline_adapted_value = _adapted_for_kline(expected_values, operator, one_hour_time_delay)\n    assert np.array_equal(operator_value, kline_adapted_value)\n    # ensure symbol parameters are used when provided\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('ETH/USDT')\"), _adapted_for_kline(expected_values / 2, operator, one_hour_time_delay)) # 1h ETH\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT')\"), kline_adapted_value) # 1h BTC\n\n    # ensure time frame is used when provided\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}(None, '4h')\"), _adapted_for_kline(expected_values * 2, operator, four_hours_time_delay)) # 4h BTC\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}(None, '1h')\"), kline_adapted_value) # 1h BTC\n\n    # ensure symbol and time frame are used when provided\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT', '1h')\"), kline_adapted_value) # 4h BTC rsi value\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('BTC/USDT', '4h')\"), _adapted_for_kline(expected_values * 2, operator, four_hours_time_delay)) # 4h BTC rsi value\n    assert np.array_equal(await _interpreter.interprete(f\"{operator}('ETH/USDT', '1h')\"), _adapted_for_kline(expected_values / 2, operator, one_hour_time_delay)) # 1h ETH rsi value\n    with pytest.raises(KeyError): # no 4h ETH candles\n        await _interpreter.interprete(f\"{operator}('ETH/USDT', '4h')\")\n\n    # with unknown kline time: unknown kline is ignored\n    def _get_kline(symbol_data, time_frame):\n        kline = octobot_trading.api.get_symbol_klines(symbol_data, time_frame)\n        kline[octobot_commons.enums.PriceIndexes.IND_PRICE_TIME.value] = 1000\n        return kline\n\n    bot_log_mock = mock.Mock(\n        error=mock.Mock()\n    )\n    with mock.patch.object(\n        ohlcv_operators, \"_get_kline\", side_effect=_get_kline\n    ) as _get_kline_mock, mock.patch.object(\n        octobot_commons.logging, \"get_logger\", mock.Mock(return_value=bot_log_mock)\n    ):\n        operator_value = await _interpreter.interprete(operator)\n        _get_kline_mock.assert_called_once()\n        # not == kline adapted value because unknown kline is ignored\n        assert np.array_equal(operator_value, kline_adapted_value) is False\n        assert np.array_equal(operator_value, expected_values)\n        bot_log_mock.error.assert_called_once()\n        assert \"kline time (1000) is not equal to last candle time not the last time\" in bot_log_mock.error.call_args[0][0]\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator\", [\n    (\"open\"),\n    (\"high\"),\n    (\"low\"),\n    (\"close\"), \n    (\"volume\"),\n    (\"time\")\n])\nasync def test_ohlcv_operators_dependencies(interpreter, operator, exchange_manager_with_candles):\n    interpreter.prepare(f\"{operator}\")\n    assert interpreter.get_dependencies() == [\n        exchange_operators.ExchangeDataDependency(\n            exchange_manager_id=octobot_trading.api.get_exchange_manager_id(exchange_manager_with_candles),\n            symbol=SYMBOL,\n            time_frame=TIME_FRAME,\n            data_source=octobot_trading.constants.OHLCV_CHANNEL\n        )\n    ]\n\n    # same dependency for all operators\n    interpreter.prepare(f\"{operator} + close + volume\")\n    assert interpreter.get_dependencies() == [\n        exchange_operators.ExchangeDataDependency(\n            exchange_manager_id=octobot_trading.api.get_exchange_manager_id(exchange_manager_with_candles),\n            symbol=SYMBOL,\n            time_frame=TIME_FRAME,\n            data_source=octobot_trading.constants.OHLCV_CHANNEL\n        )\n    ]\n\n    # SYMBOL + ETH/USDT dependency\n    # => dynamic dependencies are not yet supported. Update this test when supported.\n    interpreter.prepare(f\"{operator} + close('ETH/USDT') + volume\")\n    assert interpreter.get_dependencies() == [\n        exchange_operators.ExchangeDataDependency(\n            exchange_manager_id=octobot_trading.api.get_exchange_manager_id(exchange_manager_with_candles),\n            symbol=SYMBOL,\n            time_frame=TIME_FRAME,\n            data_source=octobot_trading.constants.OHLCV_CHANNEL\n        ),\n        # not identified as a dependency\n        # exchange_operators.ExchangeDataDependency(\n        #     exchange_manager_id=octobot_trading.api.get_exchange_manager_id(exchange_manager_with_candles),\n        #     symbol=\"ETH/USDT\",\n        #     time_frame=TIME_FRAME,\n        #     data_source=octobot_trading.constants.OHLCV_CHANNEL\n        # ),\n    ]\n"
  },
  {
    "path": "Meta/DSL_operators/exchange_operators/tests/test_mocks.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport numpy as np\n\nimport octobot_commons.enums\nimport octobot_commons.constants\n\nfrom tentacles.Meta.DSL_operators.exchange_operators.tests import (\n    historical_prices,\n    historical_volume,\n    historical_times,\n    KLINE_SIGNATURE,\n    TIME_FRAME,\n    exchange_manager_with_candles,\n    exchange_manager_with_candles_and_klines,\n    exchange_manager_with_candles_and_new_candle_klines,\n    candle_manager_by_time_frame_by_symbol,\n    interpreter,\n    interpreter_with_candle_manager_by_time_frame_by_symbol,\n    interpreter_with_exchange_manager_and_new_candle_klines,\n    interpreter_with_exchange_manager_and_klines\n)\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_mock(interpreter, historical_prices, historical_volume, historical_times):\n    assert np.array_equal(await interpreter.interprete(\"open\"), historical_prices)\n    assert await interpreter.interprete(\"open[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter.interprete(\"high\"), historical_prices)\n    assert await interpreter.interprete(\"high[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter.interprete(\"low\"), historical_prices)\n    assert await interpreter.interprete(\"low[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter.interprete(\"close\"), historical_prices)\n    assert await interpreter.interprete(\"close[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter.interprete(\"volume\"), historical_volume)\n    assert await interpreter.interprete(\"volume[-1]\") == historical_volume[-1] == 1113\n    assert np.array_equal(await interpreter.interprete(\"time\"), historical_times)\n    assert await interpreter.interprete(\"time[-1]\") == historical_times[-1] == 41\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_with_exchange_manager_and_klines_mock(\n    interpreter_with_exchange_manager_and_klines, historical_prices, historical_volume, historical_times\n):\n    kline_adapted_historical_prices = historical_prices.copy()\n    kline_adapted_historical_prices[-1] += KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_klines.interprete(\"open\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_klines.interprete(\"open[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_klines.interprete(\"high\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_klines.interprete(\"high[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_klines.interprete(\"low\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_klines.interprete(\"low[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_klines.interprete(\"close\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_klines.interprete(\"close[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    kline_adapted_historical_volume = historical_volume.copy()\n    kline_adapted_historical_volume[-1] += KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_klines.interprete(\"volume\"), \n    kline_adapted_historical_volume)\n    assert await interpreter_with_exchange_manager_and_klines.interprete(\"volume[-1]\") == historical_volume[-1] + KLINE_SIGNATURE == 1113 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_klines.interprete(\"time\"), historical_times)\n    assert await interpreter_with_exchange_manager_and_klines.interprete(\"time[-1]\") == historical_times[-1] == 41\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_with_exchange_manager_and_new_candle_klines_mock(\n    interpreter_with_exchange_manager_and_new_candle_klines, historical_prices, historical_volume, historical_times\n):\n    kline_adapted_historical_prices = np.append(historical_prices[1:], historical_prices[-1] + KLINE_SIGNATURE)\n    assert len(historical_prices) == len(kline_adapted_historical_prices)\n    assert np.array_equal(await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"open\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"open[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"high\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"high[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"low\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"low[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    assert np.array_equal(await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"close\"), kline_adapted_historical_prices)\n    assert await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"close[-1]\") == kline_adapted_historical_prices[-1] == 92.22 + KLINE_SIGNATURE\n    kline_adapted_historical_volume = np.append(historical_volume[1:], historical_volume[-1] + KLINE_SIGNATURE)\n    assert np.array_equal(await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"volume\"), \n    kline_adapted_historical_volume)\n    assert await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"volume[-1]\") == historical_volume[-1] + KLINE_SIGNATURE == 1113 + KLINE_SIGNATURE\n    new_kline_time = historical_times[-1] + octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames(TIME_FRAME)] * octobot_commons.constants.MINUTE_TO_SECONDS\n    kline_adapted_historical_times = np.append(historical_times[1:], new_kline_time)\n    assert np.array_equal(await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"time\"), kline_adapted_historical_times)\n    assert await interpreter_with_exchange_manager_and_new_candle_klines.interprete(\"time[-1]\") == kline_adapted_historical_times[-1] == new_kline_time\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_with_candle_manager_by_time_frame_by_symbol_mock(\n    interpreter_with_candle_manager_by_time_frame_by_symbol, historical_prices, historical_volume, historical_times\n):\n    assert np.array_equal(await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"open\"), historical_prices)\n    assert await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"open[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"high\"), historical_prices)\n    assert await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"high[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"low\"), historical_prices)\n    assert await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"low[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"close\"), historical_prices)\n    assert await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"close[-1]\") == historical_prices[-1] == 92.22\n    assert np.array_equal(await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"volume\"), historical_volume)\n    assert await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"volume[-1]\") == historical_volume[-1] == 1113\n    assert np.array_equal(await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"time\"), historical_times)\n    assert await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete(\"time[-1]\") == historical_times[-1] == 41\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/__init__.py",
    "content": "# pylint: disable=R0801\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.DSL_operators.python_std_operators.base_binary_operators as dsl_interpreter_base_binary_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_binary_operators import (\n    AddOperator,\n    SubOperator,\n    MultOperator,\n    DivOperator,\n    FloorDivOperator,\n    ModOperator,\n    PowOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_compare_operators as dsl_interpreter_base_compare_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_compare_operators import (\n    EqOperator,\n    NotEqOperator,\n    LtOperator,\n    LtEOperator,\n    GtOperator,\n    GtEOperator,\n    IsOperator,\n    IsNotOperator,\n    InOperator,\n    NotInOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_unary_operators as dsl_interpreter_base_unary_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_unary_operators import (\n    UAddOperator,\n    USubOperator,\n    NotOperator,\n    InvertOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_nary_operators as dsl_interpreter_base_nary_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_nary_operators import (\n    AndOperator,\n    OrOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_call_operators as dsl_interpreter_base_call_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_call_operators import (\n    MinOperator,\n    MaxOperator,\n    MeanOperator,\n    SqrtOperator,\n    AbsOperator,\n    RoundOperator,\n    FloorOperator,\n    CeilOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_name_operators as dsl_interpreter_base_name_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_name_operators import (\n    PiOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_expression_operators as dsl_interpreter_base_expression_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_expression_operators import (\n    IfExpOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_subscripting_operators as dsl_interpreter_base_subscripting_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_subscripting_operators import (\n    SubscriptOperator,\n    SliceOperator,\n)\nimport tentacles.Meta.DSL_operators.python_std_operators.base_iterable_operators as dsl_interpreter_base_iterable_operators\nfrom tentacles.Meta.DSL_operators.python_std_operators.base_iterable_operators import (\n    ListOperator,\n)\n\n__all__ = [\n    \"AddOperator\",\n    \"SubOperator\",\n    \"MultOperator\",\n    \"DivOperator\",\n    \"FloorDivOperator\",\n    \"ModOperator\",\n    \"PowOperator\",\n    \"EqOperator\",\n    \"NotEqOperator\",\n    \"LtOperator\",\n    \"LtEOperator\",\n    \"GtOperator\",\n    \"GtEOperator\",\n    \"IsOperator\",\n    \"IsNotOperator\",\n    \"InOperator\",\n    \"NotInOperator\",\n    \"UAddOperator\",\n    \"USubOperator\",\n    \"NotOperator\",\n    \"InvertOperator\",\n    \"AndOperator\",\n    \"OrOperator\",\n    \"MinOperator\",\n    \"MaxOperator\",\n    \"MeanOperator\",\n    \"SqrtOperator\",\n    \"AbsOperator\",\n    \"RoundOperator\",\n    \"FloorOperator\",\n    \"CeilOperator\",\n    \"PiOperator\",\n    \"IfExpOperator\",\n    \"SubscriptOperator\",\n    \"SliceOperator\",\n    \"ListOperator\",\n]\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_binary_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\n\nimport octobot_commons.dsl_interpreter.operators.binary_operator as dsl_interpreter_binary_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass AddOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"+\"\n    DESCRIPTION = \"Addition operator. Adds two operands together.\"\n    EXAMPLE = \"5 + 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Add.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left + right\n\n\nclass SubOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"-\"\n    DESCRIPTION = \"Subtraction operator. Subtracts the right operand from the left operand.\"\n    EXAMPLE = \"5 - 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Sub.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left - right\n\n\nclass MultOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"*\"\n    DESCRIPTION = \"Multiplication operator. Multiplies two operands.\"\n    EXAMPLE = \"5 * 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Mult.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left * right\n\n\nclass DivOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"/\"\n    DESCRIPTION = \"Division operator. Divides the left operand by the right operand.\"\n    EXAMPLE = \"10 / 2\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Div.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left / right\n\n\nclass FloorDivOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"//\"\n    DESCRIPTION = \"Floor division operator. Divides the left operand by the right operand and returns the floor of the result.\"\n    EXAMPLE = \"10 // 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.FloorDiv.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left // right\n\n\nclass ModOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"%\"\n    DESCRIPTION = \"Modulo operator. Returns the remainder after dividing the left operand by the right operand.\"\n    EXAMPLE = \"10 % 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Mod.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left % right\n\n\nclass PowOperator(dsl_interpreter_binary_operator.BinaryOperator):\n    NAME = \"**\"\n    DESCRIPTION = \"Exponentiation operator. Raises the left operand to the power of the right operand.\"\n    EXAMPLE = \"2 ** 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Pow.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left**right\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_call_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport math\n\nimport octobot_commons.errors\nimport octobot_commons.dsl_interpreter as dsl_interpreter\n\n\nclass MinOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    NAME = \"min\"\n    DESCRIPTION = \"Returns the minimum value from the given operands.\"\n    EXAMPLE = \"min(1, 2, 3)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"min\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return min(operands)\n\n\nclass MaxOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    NAME = \"max\"\n    DESCRIPTION = \"Returns the maximum value from the given operands.\"\n    EXAMPLE = \"max(1, 2, 3)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"max\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return max(operands)\n\n\nclass MeanOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    NAME = \"mean\"\n    DESCRIPTION = \"Returns the arithmetic mean (average) of the given numeric operands.\"\n    EXAMPLE = \"mean(1, 2, 3, 4)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"mean\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        # Ensure all operands are numeric\n        numeric_operands = []\n        for operand in operands:\n            if isinstance(operand, (int, float)):\n                numeric_operands.append(operand)\n            else:\n                raise octobot_commons.errors.InvalidParametersError(\n                    f\"mean() requires numeric arguments, got {type(operand).__name__}\"\n                )\n        return sum(numeric_operands) / len(numeric_operands)\n\n\nclass SqrtOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    MAX_PARAMS = 1\n    NAME = \"sqrt\"\n    DESCRIPTION = \"Returns the square root of the given numeric operand.\"\n    EXAMPLE = \"sqrt(16)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"sqrt\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        computed_parameters = self.get_computed_parameters()\n        operand = computed_parameters[0]\n        if isinstance(operand, (int, float)):\n            return math.sqrt(operand)\n        raise octobot_commons.errors.InvalidParametersError(\n            f\"sqrt() requires a numeric argument, got {type(operand).__name__}\"\n        )\n\n\nclass AbsOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    MAX_PARAMS = 1\n    NAME = \"abs\"\n    DESCRIPTION = \"Returns the absolute value of the given operand.\"\n    EXAMPLE = \"abs(-5)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"abs\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        computed_parameters = self.get_computed_parameters()\n        operand = computed_parameters[0]\n        return abs(operand)\n\n\nclass RoundOperator(dsl_interpreter.CallOperator):\n    NAME = \"round\"\n    DESCRIPTION = \"Rounds the given numeric value to the specified number of decimal digits. If digits is not provided, rounds to the nearest integer.\"\n    EXAMPLE = \"round(3.14159, 2)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"round\"\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"value\", description=\"the value to round\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"digits\", description=\"the number of digits to round to\", required=False, type=int),\n        ]\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        computed_parameters = self.get_computed_parameters()\n        operand = computed_parameters[0]\n        digits = int(computed_parameters[1]) if len(computed_parameters) == 2 else 0\n        if isinstance(operand, (int, float)):\n            return round(operand, digits)\n        raise octobot_commons.errors.InvalidParametersError(\n            f\"round() requires a numeric argument, got {type(operand).__name__}\"\n        )\n\n\nclass FloorOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    MAX_PARAMS = 1\n    NAME = \"floor\"\n    DESCRIPTION = \"Returns the floor of the given numeric operand (largest integer less than or equal to the value).\"\n    EXAMPLE = \"floor(3.7)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"floor\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        computed_parameters = self.get_computed_parameters()\n        operand = computed_parameters[0]\n        if isinstance(operand, (int, float)):\n            return math.floor(operand)\n        raise octobot_commons.errors.InvalidParametersError(\n            f\"floor() requires a numeric argument, got {type(operand).__name__}\"\n        )\n\n\nclass CeilOperator(dsl_interpreter.CallOperator):\n    MIN_PARAMS = 1\n    MAX_PARAMS = 1\n    NAME = \"ceil\"\n    DESCRIPTION = \"Returns the ceiling of the given numeric operand (smallest integer greater than or equal to the value).\"\n    EXAMPLE = \"ceil(3.2)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"ceil\"\n\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        computed_parameters = self.get_computed_parameters()\n        operand = computed_parameters[0]\n        if isinstance(operand, (int, float)):\n            return math.ceil(operand)\n        raise octobot_commons.errors.InvalidParametersError(\n            f\"ceil() requires a numeric argument, got {type(operand).__name__}\"\n        )\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_compare_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\n\nimport octobot_commons.dsl_interpreter.operators.compare_operator as dsl_interpreter_compare_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass EqOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"==\"\n    DESCRIPTION = \"Equality operator. Returns True if the left operand equals the right operand.\"\n    EXAMPLE = \"5 == 5\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Eq.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left == right\n\n\nclass NotEqOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"!=\"\n    DESCRIPTION = \"Inequality operator. Returns True if the left operand does not equal the right operand.\"\n    EXAMPLE = \"5 != 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.NotEq.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left != right\n\n\nclass LtOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"<\"\n    DESCRIPTION = \"Less than operator. Returns True if the left operand is less than the right operand.\"\n    EXAMPLE = \"3 < 5\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Lt.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left < right\n\n\nclass LtEOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"<=\"\n    DESCRIPTION = \"Less than or equal operator. Returns True if the left operand is less than or equal to the right operand.\"\n    EXAMPLE = \"5 <= 5\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.LtE.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left <= right\n\n\nclass GtOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \">\"\n    DESCRIPTION = \"Greater than operator. Returns True if the left operand is greater than the right operand.\"\n    EXAMPLE = \"5 > 3\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Gt.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left > right\n\n\nclass GtEOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \">=\"\n    DESCRIPTION = \"Greater than or equal operator. Returns True if the left operand is greater than or equal to the right operand.\"\n    EXAMPLE = \"5 >= 5\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.GtE.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left >= right\n\n\nclass IsOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"is\"\n    DESCRIPTION = \"Identity operator. Returns True if the left operand is the same object as the right operand.\"\n    EXAMPLE = \"x is None\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Is.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left is right\n\n\nclass IsNotOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"is not\"\n    DESCRIPTION = \"Negated identity operator. Returns True if the left operand is not the same object as the right operand.\"\n    EXAMPLE = \"x is not None\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.IsNot.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left is not right\n\n\nclass InOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"in\"\n    DESCRIPTION = \"Membership operator. Returns True if the left operand is found in the right operand (container).\"\n    EXAMPLE = \"3 in [1, 2, 3]\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.In.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left in right\n\n\nclass NotInOperator(dsl_interpreter_compare_operator.CompareOperator):\n    NAME = \"not in\"\n    DESCRIPTION = \"Negated membership operator. Returns True if the left operand is not found in the right operand (container).\"\n    EXAMPLE = \"4 not in [1, 2, 3]\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.NotIn.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        left, right = self.get_computed_left_and_right_parameters()\n        return left not in right\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_expression_operators.py",
    "content": "# pylint: disable=missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\n\nimport octobot_commons.dsl_interpreter.operators.expression_operator as dsl_interpreter_expression_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass IfExpOperator(dsl_interpreter_expression_operator.ExpressionOperator):\n    \"\"\"\n    Base class for if expression operators: a if b else c\n    If expression operators have three operands: condition, true expression, false expression.\n    \"\"\"\n    NAME = \"if ... else\"\n    DESCRIPTION = \"Conditional expression operator. Returns the body expression if the test condition is True, otherwise returns the orelse expression.\"\n    EXAMPLE = \"5 if True else 3\"\n\n    def __init__(\n        self,\n        test: dsl_interpreter_operator.OperatorParameterType,\n        body: dsl_interpreter_operator.OperatorParameterType,\n        orelse: dsl_interpreter_operator.OperatorParameterType,\n    ):\n        super().__init__(test, body, orelse)\n        self.test = test\n        self.body = body\n        self.orelse = orelse\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.IfExp.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        # Compute the test condition\n        test_value = (\n            self.test.compute()\n            if isinstance(self.test, dsl_interpreter_operator.Operator)\n            else self.test\n        )\n        # Evaluate the condition (truthy check)\n        if test_value:\n            # Return body if condition is True\n            return (\n                self.body.compute()\n                if isinstance(self.body, dsl_interpreter_operator.Operator)\n                else self.body\n            )\n        # Return orelse if condition is False\n        return (\n            self.orelse.compute()\n            if isinstance(self.orelse, dsl_interpreter_operator.Operator)\n            else self.orelse\n        )\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_iterable_operators.py",
    "content": "# pylint: disable=missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\n\nimport octobot_commons.dsl_interpreter.operators.iterable_operator as dsl_interpreter_iterable_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass ListOperator(dsl_interpreter_iterable_operator.IterableOperator):\n    \"\"\"\n    List operator: [1, 2, 3]\n    List operator have one or more operands.\n    \"\"\"\n    NAME = \"[...]\"\n    DESCRIPTION = \"List constructor operator. Creates a list from the given operands.\"\n    EXAMPLE = \"[1, 2, 3]\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.List.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        # Compute the test condition\n        return list(self.get_computed_parameters())\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_name_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport math\n\nimport octobot_commons.dsl_interpreter.operators.name_operator as dsl_interpreter_name_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass PiOperator(dsl_interpreter_name_operator.NameOperator):\n    MAX_PARAMS = 0\n    NAME = \"pi\"\n    DESCRIPTION = \"Mathematical constant pi (π), approximately 3.14159.\"\n    EXAMPLE = \"pi\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"pi\"\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        return math.pi\n\n\nclass NaNOperator(dsl_interpreter_name_operator.NameOperator):\n    MAX_PARAMS = 0\n    NAME = \"nan\"\n    DESCRIPTION = \"Not a Number constant. Represents an undefined or unrepresentable numeric value.\"\n    EXAMPLE = \"nan\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"nan\"\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        return float(\"nan\")\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_nary_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\n\nimport octobot_commons.dsl_interpreter.operators.n_ary_operator as dsl_interpreter_n_ary_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass AndOperator(dsl_interpreter_n_ary_operator.NaryOperator):\n    MIN_PARAMS = 1\n    MAX_PARAMS = None\n    NAME = \"and\"\n    DESCRIPTION = \"Logical AND operator. Returns True if all operands are truthy, otherwise returns False.\"\n    EXAMPLE = \"True and False\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.And.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return all(operands)\n\n\nclass OrOperator(dsl_interpreter_n_ary_operator.NaryOperator):\n    MIN_PARAMS = 1\n    MAX_PARAMS = None\n    NAME = \"or\"\n    DESCRIPTION = \"Logical OR operator. Returns True if any operand is truthy, otherwise returns False.\"\n    EXAMPLE = \"True or False\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Or.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return any(operands)\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_subscripting_operators.py",
    "content": "# pylint: disable=missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\nimport numpy as np\nimport typing\n\nimport octobot_commons.errors\nimport octobot_commons.dsl_interpreter.operators.subscripting_operator as dsl_interpreter_subscripting_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass SubscriptOperator(dsl_interpreter_subscripting_operator.SubscriptingOperator):\n    \"\"\"\n    Base class for subscripting operators: array[index]\n    Subscripting operators have three operands: the array/list, the index or slice and the context.\n    \"\"\"\n    NAME = \"[...]\"\n    DESCRIPTION = \"Subscripting operator. Accesses an element from a list or array using an index.\"\n    EXAMPLE = \"my_list[0]\"\n\n    def __init__(\n        self,\n        array_or_list: dsl_interpreter_operator.OperatorParameterType,\n        index_or_slice: dsl_interpreter_operator.OperatorParameterType,\n        context: dsl_interpreter_operator.OperatorParameterType,\n        **kwargs: typing.Any\n    ):\n        \"\"\"\n        Initialize the subscripting operator with its array, index and context.\n        \"\"\"\n        super().__init__(array_or_list, index_or_slice, context, **kwargs)\n\n    def get_computed_array_or_list_and_index_or_slice_and_context_parameters(\n        self,\n    ) -> typing.Tuple[\n        dsl_interpreter_operator.ComputedOperatorParameterType,\n        dsl_interpreter_operator.ComputedOperatorParameterType,\n        dsl_interpreter_operator.ComputedOperatorParameterType,\n    ]:\n        \"\"\"\n        Get the computed array/list, index/slice and context of the subscripting operator.\n        \"\"\"\n        computed_parameters = self.get_computed_parameters()\n        if len(computed_parameters) != 3:\n            raise octobot_commons.errors.InvalidParametersError(f\"Unsupported {self.__class__.__name__}: expected three parameters, got {len(computed_parameters)}\")\n        if not isinstance(computed_parameters, (list, tuple, np.ndarray)):\n            raise octobot_commons.errors.InvalidParametersError(f\"Unsupported {self.__class__.__name__} computed parameters 1 type: {type(computed_parameters).__name__}\")\n        return computed_parameters[0], computed_parameters[1], computed_parameters[2]\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Subscript.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        # Compute the test condition\n        array_or_list, index, context = self.get_computed_array_or_list_and_index_or_slice_and_context_parameters()\n        if isinstance(context, ast.Load):\n            return array_or_list[index]\n        raise octobot_commons.errors.InvalidParametersError(f\"Unsupported {self.__class__.__name__} context type: {type(context).__name__}\")\n\n\nclass SliceOperator(dsl_interpreter_subscripting_operator.SubscriptingOperator):\n    \"\"\"\n    Operator for creating slice objects: slice(lower, upper, step)\n    Used for array slicing like array[start:stop:step]\n    \"\"\"\n    NAME = \"[start:stop:step]\"\n    DESCRIPTION = \"Slice operator. Creates a slice object for array/list slicing with optional start, stop, and step parameters.\"\n    EXAMPLE = \"my_list[1:5:2]\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Slice.__name__\n\n    def get_computed_lower_and_upper_and_step_parameters(\n        self,\n    ) -> typing.Tuple[\n        dsl_interpreter_operator.ComputedOperatorParameterType,\n        dsl_interpreter_operator.ComputedOperatorParameterType,\n        dsl_interpreter_operator.ComputedOperatorParameterType,\n    ]:\n        \"\"\"\n        Get the computed lower, upper and step of the slice operator.\n        \"\"\"\n        computed_parameters = self.get_computed_parameters()\n        if len(computed_parameters) > 3:\n            raise octobot_commons.errors.InvalidParametersError(f\"Unsupported {self.__class__.__name__}: expected at most three parameters, got {len(computed_parameters)}\")\n        lower = int(computed_parameters[0]) if len(computed_parameters) > 0 and computed_parameters[0] is not None else None\n        upper = int(computed_parameters[1]) if len(computed_parameters) > 1 and computed_parameters[1] is not None else None\n        step = int(computed_parameters[2]) if len(computed_parameters) > 2 and computed_parameters[2] is not None else None\n        return lower, upper, step\n\n    def compute(self) -> slice:\n        \"\"\"\n        Compute and return a Python slice object.\n        \"\"\"\n        maybe_lower, maybe_upper, maybe_step = self.get_computed_lower_and_upper_and_step_parameters()\n        if maybe_lower is not None:\n            if maybe_upper is not None:\n                if maybe_step is not None:\n                    return slice(maybe_lower, maybe_upper, maybe_step)\n                return slice(maybe_lower, maybe_upper, None)\n            return slice(maybe_lower, None, None)\n        if maybe_upper is not None:\n            return slice(None, maybe_upper, None)\n        return slice(None, None, None)\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/base_unary_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ast\n\nimport octobot_commons.dsl_interpreter.operators.unary_operator as dsl_interpreter_unary_operator\nimport octobot_commons.dsl_interpreter.operator as dsl_interpreter_operator\n\n\nclass UAddOperator(dsl_interpreter_unary_operator.UnaryOperator):\n    NAME = \"+\"\n    DESCRIPTION = \"Unary plus operator. Returns the operand unchanged (mainly for symmetry with unary minus).\"\n    EXAMPLE = \"+5\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.UAdd.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        operand = self.get_computed_operand()\n        return +operand\n\n\nclass USubOperator(dsl_interpreter_unary_operator.UnaryOperator):\n    NAME = \"-\"\n    DESCRIPTION = \"Unary minus operator. Negates the operand (multiplies by -1).\"\n    EXAMPLE = \"-5\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.USub.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        operand = self.get_computed_operand()\n        return -operand\n\n\nclass NotOperator(dsl_interpreter_unary_operator.UnaryOperator):\n    NAME = \"not\"\n    DESCRIPTION = \"Logical NOT operator. Returns True if the operand is falsy, False if it is truthy.\"\n    EXAMPLE = \"not True\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Not.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        operand = self.get_computed_operand()\n        return not operand\n\n\nclass InvertOperator(dsl_interpreter_unary_operator.UnaryOperator):\n    NAME = \"~\"\n    DESCRIPTION = \"Bitwise NOT operator. Inverts all bits of the operand. In this implementation, it behaves as logical NOT.\"\n    EXAMPLE = \"~True\"\n\n    @staticmethod\n    def get_name() -> str:\n        return ast.Invert.__name__\n\n    def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:\n        operand = self.get_computed_operand()\n        return not operand  # ~operand has been deprecated in favor of \"not\"\n        # return ~operand\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/tests/test_base_operators.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport math\nimport pytest\nimport octobot_commons.dsl_interpreter as dsl_interpreter\nimport octobot_commons.errors\n\n\n@pytest.fixture\ndef interpreter():\n    return dsl_interpreter.Interpreter(dsl_interpreter.get_all_operators())\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_basic_operations(interpreter):\n    # constants\n    assert await interpreter.interprete(\"True\") is True\n    assert await interpreter.interprete(\"'test'\") == \"test\"\n    assert await interpreter.interprete('\"test\"') == \"test\"\n\n    # unary operators\n    assert await interpreter.interprete(\"1\") == 1\n    assert await interpreter.interprete(\"-11\") == -11\n    assert await interpreter.interprete(\"+11\") == +11\n    assert await interpreter.interprete(\"not True\") is False\n    assert await interpreter.interprete(\"~ False\") is True\n\n    # binary operators\n    assert await interpreter.interprete(\"1 + 2\") == 3\n    assert await interpreter.interprete(\"1 - 2\") == -1\n    assert await interpreter.interprete(\"4 * 2\") == 8\n    assert await interpreter.interprete(\"1 / 2\") == 0.5\n    assert await interpreter.interprete(\"1 % 3\") == 1\n    assert await interpreter.interprete(\"1 // 2\") == 0\n    assert await interpreter.interprete(\"3 ** 2\") == 9\n\n    # compare operators\n    assert await interpreter.interprete(\"1 < 2\") is True\n    assert await interpreter.interprete(\"1 <= 2\") is True\n    assert await interpreter.interprete(\"2 <= 2\") is True\n    assert await interpreter.interprete(\"1 > 2\") is False\n    assert await interpreter.interprete(\"2 >= 2\") is True\n    assert await interpreter.interprete(\"1 == 2\") is False\n    assert await interpreter.interprete(\"1 != 2\") is True\n    assert await interpreter.interprete(\"1 is 2\") is False\n    assert await interpreter.interprete(\"1 is not 2\") is True\n    assert await interpreter.interprete(\"'1' in '123'\") is True\n    assert await interpreter.interprete(\"'4' in '123'\") is False\n    assert await interpreter.interprete(\"1 in [1, 2, 3]\") is True\n    assert await interpreter.interprete(\"4 in [1, 2, 3]\") is False\n    assert await interpreter.interprete(\"1 not in [1, 2, 3]\") is False\n    assert await interpreter.interprete(\"4 not in [1, 2, 3]\") is True\n\n    # variables\n    assert await interpreter.interprete(\"pi\") == math.pi\n    assert await interpreter.interprete(\"pi + 1\") == math.pi + 1\n    assert math.isnan(await interpreter.interprete(\"nan\"))\n    assert math.isnan(await interpreter.interprete(\"nan + 1\"))\n\n    # expressions\n    assert await interpreter.interprete(\"1 if True else 2\") == 1\n    assert await interpreter.interprete(\"1 if False else 2\") == 2\n    assert await interpreter.interprete(\"1 if 1 < 2 else 2\") == 1\n    assert await interpreter.interprete(\"1 if 1 > 2 else 2\") == 2\n    assert await interpreter.interprete(\"1 if 1 == 1 else 2\") == 1\n    assert await interpreter.interprete(\"1 if 1 != 2 else 2\") == 1\n    assert await interpreter.interprete(\"1 if 1 is 1 else 2\") == 1\n    assert await interpreter.interprete(\"1 if 1 is not 2 else 2\") == 1\n\n    # subscripting operators\n    assert await interpreter.interprete(\"[1, 2, 3][:]\") == [1, 2, 3]\n    assert await interpreter.interprete(\"[1, 2, 3][0]\") == 1\n    assert await interpreter.interprete(\"[1, 2, 3][0:2]\") == [1, 2]\n    assert await interpreter.interprete(\"[1, 2, 3][2:]\") == [3]\n    assert await interpreter.interprete(\"[1, 2, 3][:1]\") == [1]\n    assert await interpreter.interprete(\"[1, 2, 3][:-1]\") == [1, 2]\n    assert await interpreter.interprete(\"[1, 2, 3][-1]\") == 3\n    assert await interpreter.interprete(\"[1, 2, 3, 4, 5, 6][0:6:2]\") == [1, 3, 5]\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_mixed_basic_operations(interpreter):\n    assert await interpreter.interprete(\"1 + 2 * 3\") == 7\n    assert await interpreter.interprete(\"(1 + 2) * 3\") == 9\n    assert await interpreter.interprete(\"(1 + 2) * 3 + 5 / 2 + 10\") == 21.5\n    assert await interpreter.interprete(\"(1 + 2) * 3 if 1 < 2 else 10 + pi\") == 9\n    assert await interpreter.interprete(\"(1 + 2) * 3 if 1 > 2 else 10 + pi\") == 10 + math.pi\n    assert await interpreter.interprete(\"1 < 2 and 2 < 3\") is True\n    assert await interpreter.interprete(\"1 < 2 and 2 < 3 and True and 1\") is True\n    assert await interpreter.interprete(\"1 < 2 and 2 > 3\") is False\n    assert await interpreter.interprete(\"1 < 2 or 2 > 3\") is True\n    assert await interpreter.interprete(\"1 < 2 or 2 > 3 or True or False or 0\") is True\n    assert await interpreter.interprete(\"1 > 2 or 2 > 3\") is False\n    assert await interpreter.interprete(\"not (1 < 2 and 2 < 3)\") is False\n    assert await interpreter.interprete(\"not (1 < 2 and 2 > 3)\") is True\n    assert await interpreter.interprete(\"not (1 > 2 or 2 > 3)\") is True\n    assert await interpreter.interprete(\"not (1 > 2 or 2 < 3)\") is False\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_call_operations(interpreter):\n    assert await interpreter.interprete(\"max(1, 2, 3)\") == 3\n    assert await interpreter.interprete(\"min(1, 2, 3)\") == 1\n    assert await interpreter.interprete(\"abs(-1)\") == 1\n    assert await interpreter.interprete(\"abs(1)\") == 1\n    assert await interpreter.interprete(\"sqrt(4)\") == 2\n    assert await interpreter.interprete(\"mean(1, 2, 3)\") == 2\n    assert await interpreter.interprete(\"mean(50, 110.2)\") == 80.1\n    assert await interpreter.interprete(\"mean(3)\") == 3\n    assert await interpreter.interprete(\"round(1.23456789, 2)\") == 1.23\n    assert await interpreter.interprete(\"round(1.23456789, 2.22)\") == 1.23\n    assert await interpreter.interprete(\"round(1.23456789)\") == 1\n    assert await interpreter.interprete(\"floor(1.23456789)\") == 1\n    assert await interpreter.interprete(\"ceil(1.23456789)\") == 2\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_mixed_call_and_basic_operations(interpreter):\n    assert await interpreter.interprete(\"max(sqrt(9), abs(-4), 3 + 6)\") == 9\n    assert await interpreter.interprete(\"min(sqrt(9), abs(-4), 3 + 6)\") == 3\n    assert await interpreter.interprete(\"abs(min(sqrt(9), abs(-4), 3 + 6))\") == 3\n    assert await interpreter.interprete(\"sqrt(max(1, 2, 3, 4))\") == 2\n    assert await interpreter.interprete(\"sqrt(2**2)\") == 2\n    assert await interpreter.interprete(\"sqrt(min(1, 2, 3))\") == 1\n    assert await interpreter.interprete(\"abs(sqrt(max(1, 2, 4)))\") == 2\n    assert await interpreter.interprete(\"abs(sqrt(min(1, 2, 4)))\") == 1\n    assert await interpreter.interprete(\"mean(4, 5) + 1 + mean(1, 1 + 1, 3)\") == 7.5\n\n\n@pytest.mark.asyncio\nasync def test_interpreter_insupported_operations(interpreter):\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"1 & 2\")\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"1 | 2\")\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"3 ^ 2\")\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"1 << 2\")\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"1 >> 2\")\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"my_variable\")\n    with pytest.raises(octobot_commons.errors.UnsupportedOperatorError):\n        await interpreter.interprete(\"unknown_operator(1)\")\n    with pytest.raises(octobot_commons.errors.InvalidParametersError):\n        await interpreter.interprete(\"mean(1, 'a')\")\n    with pytest.raises(octobot_commons.errors.InvalidParametersError):\n        await interpreter.interprete(\"mean()\")\n"
  },
  {
    "path": "Meta/DSL_operators/python_std_operators/tests/test_dictionnaries.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport octobot_commons.constants\nimport octobot_commons.dsl_interpreter\n\n\n@pytest.mark.parametrize(\n    \"libraries\", \n    [tuple(), (octobot_commons.constants.BASE_OPERATORS_LIBRARY, )]\n)\ndef test_get_all_operators(libraries):\n    assert octobot_commons.dsl_interpreter.get_all_operators(*libraries) is not None\n    assert len(octobot_commons.dsl_interpreter.get_all_operators(*libraries)) > 0\n    operators = octobot_commons.dsl_interpreter.get_all_operators(*libraries)\n    operator_types = [\n        octobot_commons.dsl_interpreter.BinaryOperator,\n        octobot_commons.dsl_interpreter.UnaryOperator,\n        octobot_commons.dsl_interpreter.CompareOperator,\n        octobot_commons.dsl_interpreter.NaryOperator,\n        octobot_commons.dsl_interpreter.CallOperator,\n        octobot_commons.dsl_interpreter.NameOperator,\n    ]\n    operator_by_type = {\n        operator_type.__name__: [] for operator_type in operator_types\n    }\n    for operator in operators:\n        name = operator.get_name()\n        assert len(name) > 0\n        for operator_type in operator_types:\n            if issubclass(operator, operator_type):\n                operator_by_type[operator_type.__name__].append(operator)\n                break\n    for operator_type, operators in operator_by_type.items():\n        assert len(operators) > 1, f\"Expected at least 2 {operator_type} operators. {operator_by_type=}\"\n"
  },
  {
    "path": "Meta/DSL_operators/ta_operators/__init__.py",
    "content": "# pylint: disable=R0801\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.DSL_operators.ta_operators.tulipy_technical_analysis_operators as tulipy_technical_analysis_operators\nfrom tentacles.Meta.DSL_operators.ta_operators.tulipy_technical_analysis_operators import (\n    RSIOperator,\n)\n\n\n__all__ = [\n    \"RSIOperator\",\n]\n"
  },
  {
    "path": "Meta/DSL_operators/ta_operators/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Meta/DSL_operators/ta_operators/ta_operator.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.dsl_interpreter.operators.call_operator as dsl_interpreter_call_operator\n\n\nTA_LIBRARY = \"ta\"\n\n\nclass TAOperator(dsl_interpreter_call_operator.CallOperator):\n\n    @staticmethod\n    def get_library() -> str:\n        \"\"\"\n        Get the library of the operator.\n        \"\"\"\n        return TA_LIBRARY\n"
  },
  {
    "path": "Meta/DSL_operators/ta_operators/tests/test_docs_examples.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nfrom tentacles.Meta.DSL_operators.exchange_operators.tests import (\n    historical_prices,\n    historical_volume,\n    historical_times,\n    exchange_manager_with_candles,\n    interpreter,\n)\n\n\n@pytest.mark.asyncio\nasync def test_mm_formulas_docs_examples(interpreter):\n    # ensure examples in the docs are working (meaning returning a parsable number)\n    assert round(await interpreter.interprete(\"close[-1]\"), 2) == 92.22\n    assert round(await interpreter.interprete(\"open[-1]\"), 2) == 92.22\n    assert round(await interpreter.interprete(\"high[-3]\"), 2) == 92.92\n    assert round(await interpreter.interprete(\"low[-1]\"), 2) == 92.22\n    assert round(await interpreter.interprete(\"volume[-2]\"), 2) == 1211\n    assert round(await interpreter.interprete(\"time[-1]\"), 2) == 41\n    assert round(await interpreter.interprete(\"ma(close, 12)[-1]\"), 2) == 92.95\n    assert round(await interpreter.interprete(\"ema(open, 24)[-1]\"), 2) == 90.21\n    assert round(await interpreter.interprete(\"vwma(close, volume, 4)[-1]\"), 2) == 92.54\n    assert round(await interpreter.interprete(\"rsi(close, 14)[-1]\"), 2) == 67.55\n    assert round(await interpreter.interprete(\"max(close[-1], open[-1])\"), 2) == 92.22\n    assert round(await interpreter.interprete(\"min(ma(close, 12)[-1], ema(open, 24)[-1])\"), 2) == 90.21\n    assert round(await interpreter.interprete(\"mean(close[-1], open[-1], high[-1], low[-1])\"), 2) == 92.22\n    assert round(await interpreter.interprete(\"round(ma(close, 12)[-1], 2)\"), 2) == 92.95\n    assert round(await interpreter.interprete(\"floor(close[-1])\"), 2) == 92\n    assert round(await interpreter.interprete(\"ceil(close[-1])\"), 2) == 93\n    assert round(await interpreter.interprete(\"abs(close[-1] - open[-1])\"), 2) == 0\n    assert round(await interpreter.interprete(\"100 if close[-1] > open[-1] else (90 + 1)\"), 2) == 91\n"
  },
  {
    "path": "Meta/DSL_operators/ta_operators/tests/test_tulipy_technical_analysis_operators.py",
    "content": "#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport octobot_commons.errors\nfrom tentacles.Meta.DSL_operators.exchange_operators.tests import (\n    historical_prices,\n    historical_volume,\n    historical_times,\n    exchange_manager_with_candles,\n    interpreter,\n)\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator, static_parameters\", [\n    # list all operator and \"possible\" invalid parameters\n    (\"rsi\", [\"\", \"(close)\", \"(close, 14, 20)\"]),\n    (\"macd\", [\"\", \"(close)\", \"(close, 'a')\", \"(close, 'a', 26)\", \"('a', 14, 26, 9, 0)\"]),\n    (\"ma\", [\"\", \"(close)\", \"(close, 14, 20)\"]),\n    (\"ema\", [\"\", \"(close)\", \"(close, 14, 20)\"]),\n    (\"vwma\", [\"\", \"(close)\", \"('a', 14)\", \"(close, 'a')\", \"(close, 14, 11, 20)\"]),\n])\nasync def test_operator_invalid_static_parameters(interpreter, operator, static_parameters):\n    for param in static_parameters:\n        with pytest.raises(octobot_commons.errors.InvalidParametersError, match=f\"{operator} \"):\n            # static validation\n            interpreter.prepare(f\"{operator}{param}\")\n        with pytest.raises(octobot_commons.errors.InvalidParametersError):\n            # dynamic validation\n            await interpreter.interprete(f\"{operator}{param}\")\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator, dynamic_parameters\", [\n    # list all operator and \"possible\" invalid parameters\n    (\"rsi\", [\"('a', 14)\", \"(close, 'a')\"]),\n    (\"macd\", [\"('a', 14, 26, 9)\", \"(close, 'a', 26, 9)\", \"(close, 14, 'a', 9)\", \"(close, 14, 26, 'a')\"]),\n    (\"ma\", [\"('a', 14)\", \"(close, 'a')\"]),\n    (\"ema\", [\"('a', 14)\", \"(close, 'a')\"]),\n    (\"vwma\", [\"(close, volume, 'a')\", \"(close, 14, 20)\"]),\n])\nasync def test_operator_invalid_dynamic_parameters(interpreter, operator, dynamic_parameters):\n    for param in dynamic_parameters:\n        # static validation: do not raise\n        interpreter.prepare(f\"{operator}{param}\")\n        with pytest.raises(octobot_commons.errors.InvalidParametersError):\n            # dynamic validation\n            await interpreter.interprete(f\"{operator}{param}\")\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"operator, dynamic_parameters\", [\n    # list all operator and invalid parameters that should raise a tulipy error that will be converted to a TypeError\n    (\"rsi\", [\"(close, 999999)\", \"(close, 0)\", \"(close, -1)\"]),\n    (\"macd\", [\"(close, 14, 99999, 2)\", \"(close, 99999, 12, 2)\", \"(close, 0, 12, 2)\", \"(close, 7, 12, -1)\"]),\n    (\"ma\", [\"(close, 999999)\", \"(close, 0)\", \"(close, -1)\"]),\n    (\"ema\", [\"(close, -1)\"]),\n    (\"vwma\", [\"(close, volume, 999999)\", \"(close, volume, 0)\", \"(close, volume, -1)\"]),\n])\nasync def test_operator_converted_tulipy_error(interpreter, operator, dynamic_parameters):\n    for param in dynamic_parameters:\n        # static validation: do not raise\n        interpreter.prepare(f\"{operator}{param}\")\n        with pytest.raises(TypeError):\n            # dynamic validation\n            await interpreter.interprete(f\"{operator}{param}\")\n\n\n@pytest.mark.asyncio\nasync def test_operator_operations(interpreter):\n    # ensure the output is a list and can be used in arithmetic operations\n    assert isinstance(await interpreter.interprete(\"rsi(close, 14)\"), list)\n    assert await interpreter.interprete(\"round(rsi(close, 26)[-1], 2)\") == 74.3\n    assert await interpreter.interprete(\"round(rsi(close, 14)[-1], 2)\") == 67.55\n    assert await interpreter.interprete(\"round(rsi(close, 26)[-1] - rsi(close, 14)[-1], 2)\") == 6.74\n\n    # combine ma & vwma\n    ma = await interpreter.interprete(\"ma(close, 14)\")\n    vwma = await interpreter.interprete(\"vwma(close, volume, 14)\")\n    assert round(ma[-1], 2) == 92.53\n    assert round(vwma[-1], 2) == 92.37\n    assert round(ma[-1]*0.7 + vwma[-1]*0.3, 2) == 92.48\n    assert await interpreter.interprete(\"round(ma(close, 14)[-1]*0.7 + vwma(close, volume, 14)[-1]*0.3, 2)\") == 92.48\n\n\n@pytest.mark.asyncio\nasync def test_rsi_operator(interpreter):\n    rsi = await interpreter.interprete(\"rsi(close, 14)\")\n    rounded_rsi = [round(v, 2) for v in rsi]\n    assert rounded_rsi == [\n        79.56, 78.6, 77.04, 81.67, 82.88, 84.06, 87.44, 88.03, 85.21, 85.81, 86.73, \n        78.58, 78.71, 70.4, 72.5, 72.78, 67.78, 67.55\n    ]\n    # different periods, different result\n    rsi = await interpreter.interprete(\"rsi(close, 20)\")\n    rounded_rsi = [round(v, 2) for v in rsi]\n    assert rounded_rsi == [\n        85.71, 86.2, 84.2, 84.66, 85.37, 79.61, 79.7, 73.72, 75.04, 75.22, 71.62, 71.46\n    ]\n\n    assert await interpreter.interprete(\"round(rsi(close, 26)[-1], 2)\") == 74.3\n    assert await interpreter.interprete(\"round(rsi(close, 14)[-1], 2)\") == 67.55\n    assert await interpreter.interprete(\"round(rsi(close, 26)[-1] - rsi(close, 14)[-1], 2)\") == 6.74\n\n\n@pytest.mark.asyncio\nasync def test_macd_operator(interpreter):\n    macd = await interpreter.interprete(\"macd(close, 12, 26, 9)\")\n    rounded_macd = [round(v, 2) for v in macd]\n    assert rounded_macd == [0.0, -0.03, -0.14, -0.18, -0.22, -0.29, -0.34]\n\n    # different parameters, different result\n    macd = await interpreter.interprete(\"macd(close, 9, 26, 9)\")\n    rounded_macd = [round(v, 2) for v in macd]\n    assert rounded_macd == [\n        0.0, -0.09, -0.29, -0.36, -0.41, -0.52, -0.59\n    ]\n\n    macd = await interpreter.interprete(\"macd(close, 9, 20, 9)\")\n    rounded_macd = [round(v, 2) for v in macd]\n    assert rounded_macd == [\n        0.0, 0.26, 0.41, 0.41, 0.38, 0.36, 0.21, 0.07, -0.14, -0.23, -0.29, -0.4, -0.46\n    ]\n\n    macd = await interpreter.interprete(\"macd(close, 9, 20, 6)\")\n    rounded_macd = [round(v, 2) for v in macd]\n    assert rounded_macd == [\n        0.0, 0.23, 0.35, 0.32, 0.28, 0.25, 0.1, -0.01, -0.19, -0.24, -0.26, -0.33, -0.37\n    ]\n\n\n@pytest.mark.asyncio\nasync def test_ma_operator(interpreter):\n    ma = await interpreter.interprete(\"ma(close, 14)\")\n    rounded_ma = [round(v, 2) for v in ma]\n    assert rounded_ma == [\n        84.12, 84.53, 84.97, 85.26, 85.7, 86.13, 86.64, 87.36, 88.03, 88.63, \n        89.28, 89.9, 90.37, 90.82, 91.12, 91.52, 91.93, 92.3, 92.53\n    ]\n\n    # different periods, different result\n    ma = await interpreter.interprete(\"ma(close, 20)\")\n    rounded_ma = [round(v, 2) for v in ma]\n    assert rounded_ma == [\n        85.41, 85.98, 86.59, 87.1, 87.62, 88.15, 88.65, 89.16, 89.57, 89.98, \n        90.41, 90.75, 91.03\n    ]\n\n\n@pytest.mark.asyncio\nasync def test_vwma_operator(interpreter):\n    vwma = await interpreter.interprete(\"vwma(close, volume, 14)\")\n    rounded_vwma = [round(v, 2) for v in vwma]\n    assert rounded_vwma == [\n        # different results from ma(close, 14)\n        84.15, 84.51, 84.87, 85.29, 85.66, 86.3, 86.76, 87.37, 88.02, 88.55, \n        89.1, 89.9, 90.31, 90.87, 91.16, 91.53, 91.91, 92.19, 92.37\n    ]\n    # different periods, different result\n    vwma = await interpreter.interprete(\"vwma(close, volume, 20)\")\n    rounded_vwma = [round(v, 2) for v in vwma]\n    assert rounded_vwma == [\n        85.52, 85.93, 86.5, 87.19, 87.66, 88.06, 88.53, 89.24, 89.6, 89.9, \n        90.27, 90.84, 91.08\n    ]\n\n\n@pytest.mark.asyncio\nasync def test_ema_operator(interpreter):\n    ema = await interpreter.interprete(\"ema(close, 14)\")\n    rounded_ema = [round(v, 2) for v in ema]\n    assert rounded_ema == [\n        # different results from ma(close, 14)\n        81.59, 81.52, 81.7, 81.87, 82.1, 82.24, 82.32, 82.55, 82.81, 83.02, \n        83.35, 83.78, 84.19, 84.67, 85.02, 85.31, 85.53, 86.0, 86.49, 87.01, \n        87.78, 88.53, 89.13, 89.7, 90.29, 90.67, 91.0, 91.15, 91.37, 91.58, \n        91.67, 91.74\n    ]\n\n    # different periods, different result\n    ema = await interpreter.interprete(\"ema(close, 20)\")\n    rounded_ema = [round(v, 2) for v in ema]\n    assert rounded_ema == [\n        81.59, 81.54, 81.67, 81.79, 81.97, 82.08, 82.15, 82.33, 82.54, 82.71, \n        82.98, 83.32, 83.66, 84.05, 84.36, 84.63, 84.85, 85.25, 85.67, 86.12, \n        86.76, 87.39, 87.92, 88.45, 88.99, 89.38, 89.75, 89.97, 90.24, 90.5, \n        90.66, 90.81\n    ]\n"
  },
  {
    "path": "Meta/DSL_operators/ta_operators/tulipy_technical_analysis_operators.py",
    "content": "# pylint: disable=missing-class-docstring,missing-function-docstring\n#  Drakkar-Software OctoBot-Commons\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tulipy\nimport numpy as np\n\nimport octobot_commons.errors\nimport tentacles.Meta.DSL_operators.ta_operators.ta_operator as ta_operator\nimport octobot_commons.dsl_interpreter as dsl_interpreter\n\n\ndef _to_numpy_array(data):\n    if isinstance(data, list):\n        return np.array(data, dtype=np.float64)\n    elif isinstance(data, tuple):\n        return np.array(list(data), dtype=np.float64)\n    elif isinstance(data, np.ndarray):\n        if data.dtype != np.float64:\n            return data.astype(np.float64)\n        return data\n    else:\n        raise octobot_commons.errors.InvalidParametersError(f\"Unsupported data type: {type(data)}\")\n\n\ndef _to_int(value):\n    if isinstance(value, int):\n        return value\n    elif isinstance(value, float):\n        return int(value)\n    else:\n        raise octobot_commons.errors.InvalidParametersError(f\"Unsupported value type: {type(value)}\")\n\n\ndef converted_tulipy_error(f):\n    def converted_tulipy_error_wrapper(*args, **kwargs):\n        try:\n            return f(*args, **kwargs)\n        except tulipy.InvalidOptionError as err:\n            raise TypeError(\n                f\"Invalid technical indicator parameter - {err.__class__.__name__}\"\n            ) from err\n    return converted_tulipy_error_wrapper\n\n\nclass RSIOperator(ta_operator.TAOperator):\n    DESCRIPTION = \"Returns the Relative Strength Index (RSI) of the given array of numbers\"\n    EXAMPLE = \"rsi([100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110], 14)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"rsi\"\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"data\", description=\"the data to compute the RSI on\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"period\", description=\"the period to use for the RSI\", required=True, type=int),\n        ]\n\n    @converted_tulipy_error\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return list(tulipy.rsi(_to_numpy_array(operands[0]), period=_to_int(operands[1])))\n\n\nclass MACDOperator(ta_operator.TAOperator):\n    DESCRIPTION = \"Returns the Moving Average Convergence Divergence (MACD) of the given array of numbers\"\n    EXAMPLE = \"macd(close('BTC/USDT', '1h'), 12, 26, 9)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"macd\"\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"data\", description=\"the data to compute the MACD on\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"short_period\", description=\"the short period to use for the MACD\", required=True, type=int),\n            dsl_interpreter.OperatorParameter(name=\"long_period\", description=\"the long period to use for the MACD\", required=True, type=int),\n            dsl_interpreter.OperatorParameter(name=\"signal_period\", description=\"the signal period to use for the MACD\", required=True, type=int),\n        ]\n\n    @converted_tulipy_error\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        macd, macd_signal, macd_hist = tulipy.macd(\n            _to_numpy_array(operands[0]), short_period=_to_int(operands[1]), long_period=_to_int(operands[2]), signal_period=_to_int(operands[3])\n        )\n        return list(macd_hist)\n\n\nclass MAOperator(ta_operator.TAOperator):\n    DESCRIPTION = \"Returns the moving average of the given array of numbers\"\n    EXAMPLE = \"ma(close('BTC/USDT', '1h'), 14)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"ma\"\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"data\", description=\"the data to compute the moving average on\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"period\", description=\"the period to use for the moving average\", required=True, type=int),\n        ]\n\n    @converted_tulipy_error\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return list(tulipy.sma(_to_numpy_array(operands[0]), period=_to_int(operands[1])))\n\n\nclass EMAOperator(ta_operator.TAOperator):\n    DESCRIPTION = \"Returns the exponential moving average of the given array of numbers\"\n    EXAMPLE = \"ema(close('BTC/USDT', '1h'), 14)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"ema\"\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"data\", description=\"the data to compute the exponential moving average on\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"period\", description=\"the period to use for the exponential moving average\", required=True, type=int),\n        ]\n\n    @converted_tulipy_error\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return list(tulipy.ema(_to_numpy_array(operands[0]), period=_to_int(operands[1])))\n\n\nclass VWMAOperator(ta_operator.TAOperator):\n    DESCRIPTION = \"Returns the volume weighted moving average of the given array of numbers\"\n    EXAMPLE = \"vwma(close('BTC/USDT', '1h'), volume('BTC/USDT', '1h'), 14)\"\n\n    @staticmethod\n    def get_name() -> str:\n        return \"vwma\"\n\n    @staticmethod\n    def get_parameters() -> list[dsl_interpreter.OperatorParameter]:\n        return [\n            dsl_interpreter.OperatorParameter(name=\"data\", description=\"the data to compute the volume weighted moving average on\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"volume\", description=\"the volume data to use for the volume weighted moving average\", required=True, type=list),\n            dsl_interpreter.OperatorParameter(name=\"period\", description=\"the period to use for the volume weighted moving average\", required=True, type=int),\n        ]\n\n    @converted_tulipy_error\n    def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:\n        operands = self.get_computed_parameters()\n        return list(tulipy.vwma(_to_numpy_array(operands[0]), _to_numpy_array(operands[1]), period=_to_int(operands[2])))\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/TA/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .trigger import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/TA/trigger/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom .eval_triggered import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/TA/trigger/eval_triggered.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.errors as commons_errors\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.dict_util as dict_util\nimport octobot_evaluators.matrix as matrix\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Meta.Keywords.scripting_library.UI.inputs.triggers as triggers\n\n\n# 10000000000 = Sat, 20 Nov 2286 17:46:40 GMT to select all values\nALL_VALUES_CACHE_KEY = 10000000000.0\n\n\ndef _is_first_candle_only(context):\n    if not context.exchange_manager.is_backtesting:\n        # this is a backtesting only optimization\n        return False\n    tentacle_config = context.tentacle.get_local_config()\n    return tentacle_config.get(triggers.TRIGGER_ONLY_ON_THE_FIRST_CANDLE_KEY, False)\n\n\ndef _is_first_candle_call(context, init_key):\n    # TODO: figure out if we currently are in the 1st call of the given candle (careful with timeframes)\n    return not context.symbol_writer.are_data_initialized_by_key.get(init_key, False)\n\n\nasync def evaluator_get_result(\n        context: script_keywords.Context,\n        tentacle_class,\n        time_frame=None,\n        symbol: str = None,\n        trigger: bool = False,\n        value_key=commons_enums.CacheDatabaseColumns.VALUE.value,\n        cache_key=None,\n        config_name: str = None,\n        config: dict = None\n):\n    tentacle_class = tentacles_manager_api.get_tentacle_class_from_string(tentacle_class) \\\n        if isinstance(tentacle_class, str) else tentacle_class\n    config_name = context.get_config_name_or_default(tentacle_class, config_name)\n    init_key = _get_init_key(context, config_name)\n    is_first_candle_only = _is_first_candle_only(context)\n    should_trigger = not is_first_candle_only or (is_first_candle_only and _is_first_candle_call(context, init_key))\n    if not context.symbol_writer.are_data_initialized_by_key.get(init_key, False) or (should_trigger and trigger):\n        with context.adapted_trigger_timestamp(tentacle_class, config_name):\n            # always trigger when asked to then return the triggered evaluation return\n            return (await _trigger_single_evaluation(context, tentacle_class, value_key, cache_key,\n                                                     config_name, config, init_key))[0]\n    if tentacle_class.use_cache():\n        # try reading from cache\n        try:\n            with context.adapted_trigger_timestamp(tentacle_class, config_name):\n                await context.ensure_tentacle_cache_requirements(tentacle_class, config_name)\n                value, is_missing = await context.get_cached_value(value_key=value_key,\n                                                                   cache_key=cache_key,\n                                                                   tentacle_name=tentacle_class.__name__,\n                                                                   config_name=config_name)\n                if not is_missing:\n                    return value\n        except commons_errors.UninitializedCache as e:\n            if tentacle_class is not None and trigger is False:\n                raise commons_errors.UninitializedCache(f\"Can't read cache from {tentacle_class} before initializing \"\n                                                        f\"it. Either activate this tentacle or set the 'trigger' \"\n                                                        f\"parameter to True (error: {e})\") from None\n\n    _ensure_cache_when_set_value_key(value_key, tentacle_class)\n    # read from evaluation matrix\n    for value in _tentacle_values(context, tentacle_class, time_frame=time_frame, symbol=symbol):\n        return value\n\n\nasync def evaluator_get_results(\n        context: script_keywords.Context,\n        tentacle_class,\n        time_frame=None,\n        symbol: str = None,\n        trigger: bool = False,\n        value_key=commons_enums.CacheDatabaseColumns.VALUE.value,\n        cache_key=None,\n        limit: int = -1,\n        max_history: bool = False,\n        config_name: str = None,\n        config: dict = None\n):\n    cache_key = ALL_VALUES_CACHE_KEY if max_history else cache_key\n    tentacle_class = tentacles_manager_api.get_tentacle_class_from_string(tentacle_class) \\\n        if isinstance(tentacle_class, str) else tentacle_class\n    config_name = context.get_config_name_or_default(tentacle_class, config_name)\n    init_key = _get_init_key(context, config_name)\n    is_first_candle_only = _is_first_candle_only(context)\n    should_trigger = not is_first_candle_only or (is_first_candle_only and _is_first_candle_call(context, init_key))\n    if not context.symbol_writer.are_data_initialized_by_key.get(init_key, False) or (should_trigger and trigger):\n        with context.adapted_trigger_timestamp(tentacle_class, config_name):\n            # always trigger when asked to\n            eval_result, _ = await _trigger_single_evaluation(context, tentacle_class, value_key, cache_key,\n                                                              config_name, config, init_key)\n            if limit == 1:\n                # return already if only one value to return\n                return eval_result\n    if tentacle_class.use_cache():\n        try:\n            with context.adapted_trigger_timestamp(tentacle_class, config_name):\n                await context.ensure_tentacle_cache_requirements(tentacle_class, config_name)\n                # can return multiple values\n                return await context.get_cached_values(value_key=value_key, cache_key=cache_key, limit=limit,\n                                                       tentacle_name=tentacle_class.__name__, config_name=config_name)\n        except commons_errors.UninitializedCache:\n            if tentacle_class is not None and trigger is False:\n                raise commons_errors.UninitializedCache(f\"Can't read cache from {tentacle_class} before initializing \"\n                                                        f\"it. Either activate this tentacle or set the 'trigger' \"\n                                                        f\"parameter to True\") from None\n    _ensure_cache_when_set_value_key(value_key, tentacle_class)\n    if limit == 1:\n        # read from evaluation matrix\n        for value in _tentacle_values(context, tentacle_class, time_frame=time_frame, symbol=symbol):\n            return value\n        raise commons_errors.MissingDataError(f\"No evaluator value for {tentacle_class.__name__}\")\n    else:\n        raise commons_errors.ConfigEvaluatorError(f\"Evaluator cache is required to get more than one historical value \"\n                                                  f\"of an evaluator. Cache is disabled on {tentacle_class.__name__}\")\n\n\ndef _ensure_cache_when_set_value_key(value_key, tentacle_class):\n    if not tentacle_class.use_cache() and value_key != commons_enums.CacheDatabaseColumns.VALUE.value:\n        raise commons_errors.ConfigEvaluatorError(f\"Evaluator cache is required to read a value_key different from \"\n                                                  f\"the evaluator output evaluation. \"\n                                                  f\"Cache is disabled on {tentacle_class.__name__}\")\n\n\nasync def _trigger_single_evaluation(context, tentacle_class, value_key, cache_key, config_name, config, init_key):\n    config_name, cleaned_config_name, config, tentacles_setup_config, tentacle_config = \\\n        context.get_tentacle_config_elements(tentacle_class, config_name, config)\n    async with context.local_nested_tentacle_config(tentacle_class, config_name, True):\n        is_eval_result_set = False\n        eval_result = evaluator_instance = None\n        if cleaned_config_name not in tentacle_config or \\\n                not context.symbol_writer.are_data_initialized_by_key.get(init_key, False):\n            # always call _init_nested_call the 1st time the evaluation chain is triggered to make sure scripts\n            # are executed entirely at least once\n            # might need to merge config with tentacles_manager_api.get_tentacle_config if evaluator is\n            # not filling default config values\n            init_config = {**tentacle_config.get(cleaned_config_name, {}), **config}\n            eval_result, error, evaluator_instance = await _init_nested_call(\n                context, tentacle_class, config_name, cleaned_config_name,\n                tentacles_setup_config, tentacle_config, init_config\n            )\n            if error is None:\n                is_eval_result_set = True\n        try:\n            tentacle_config = tentacle_config[cleaned_config_name]\n        except KeyError as e:\n            raise commons_errors.ConfigEvaluatorError(f\"Missing evaluator configuration with name {e}\")\n        # apply forced config if any\n        dict_util.nested_update_dict(tentacle_config, config)\n        await script_keywords.save_user_input(\n            context,\n            config_name,\n            commons_constants.NESTED_TENTACLE_CONFIG,\n            tentacle_config,\n            {},\n            is_nested_config=context.nested_depth > 1,\n            nested_tentacle=tentacle_class.get_name()\n        )\n        if not is_eval_result_set:\n            eval_result, _, evaluator_instance = (await tentacle_class.single_evaluation(\n                tentacles_setup_config,\n                tentacle_config,\n                context=context\n            ))\n        if value_key == commons_enums.CacheDatabaseColumns.VALUE.value and cache_key is None:\n            return eval_result, evaluator_instance.specific_config\n        else:\n            value, is_missing = await context.get_cached_value(value_key=value_key,\n                                                               cache_key=cache_key,\n                                                               tentacle_name=tentacle_class.__name__,\n                                                               config_name=config_name,\n                                                               ignore_requirement=True)\n            return None if is_missing else value, evaluator_instance.specific_config\n\n\nasync def _init_nested_call(context, tentacle_class, config_name, cleaned_config_name,\n                            tentacles_setup_config, tentacle_config, config):\n    evaluation, error, evaluator_instance = await tentacle_class.single_evaluation(\n        tentacles_setup_config,\n        config,\n        context=context,\n        ignore_cache=True\n    )\n    tentacle_config[cleaned_config_name] = evaluator_instance.specific_config\n    if error is not None:\n        _invalidate_call_and_parents_init_status(context, config_name)\n    else:\n        context.symbol_writer.are_data_initialized_by_key[_get_init_key(context, config_name)] = True\n    return evaluation, error, evaluator_instance\n\n\ndef _get_init_key(context, config_name):\n    return f\"{config_name}_{context.time_frame}\"\n\n\ndef _invalidate_call_and_parents_init_status(context, config_name):\n    # set are_data_initialized_by_key to False for this evaluator and its parent calls to ensure init is called\n    # again later and the evaluator can be run entirely\n    context.symbol_writer.are_data_initialized_by_key[_get_init_key(context, config_name)] = False\n    for nested_config_name in context.nested_config_names:\n        context.symbol_writer.are_data_initialized_by_key[_get_init_key(context, nested_config_name)] = False\n\n\ndef _tentacle_values(context,\n                     tentacle_class,\n                     time_frames=None,\n                     symbols=None,\n                     time_frame=None,\n                     symbol=None):\n    tentacle_name = tentacle_class if isinstance(tentacle_class, str) else tentacle_class.get_name()\n    symbols = [context.symbol or symbol] or symbols\n    time_frames = [context.time_frame or time_frame] or time_frames\n    for symbol in symbols:\n        for time_frame in time_frames:\n            for tentacle_type in evaluators_enums.EvaluatorMatrixTypes:\n                for evaluated_ta_node in matrix.get_tentacles_value_nodes(\n                        context.matrix_id,\n                        matrix.get_tentacle_nodes(context.matrix_id,\n                                                  exchange_name=context.exchange_name,\n                                                  tentacle_type=tentacle_type.value,\n                                                  tentacle_name=tentacle_name),\n                        symbol=symbol,\n                        time_frame=time_frame):\n                    yield evaluated_ta_node.node_value\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom .inputs import *\nfrom .plots import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/inputs/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .library_user_inputs import *\nfrom .select_time_frame import *\nfrom .select_candle import *\nfrom .select_history import *\nfrom .triggers import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/inputs/library_user_inputs.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.configuration as commons_configuration\nimport tentacles.Meta.Keywords.scripting_library.TA.trigger.eval_triggered as eval_triggered\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n\ndef _find_configuration(nested_configuration, nested_config_names, element):\n    for key, config in nested_configuration.items():\n        if len(nested_config_names) == 0 and key == element:\n            return config\n        if isinstance(config, dict) and (len(nested_config_names) == 0 or key == nested_config_names[0]):\n            found_config = _find_configuration(config, nested_config_names[1:], element)\n            if found_config is not None:\n                return found_config\n    return None\n\n\nasync def external_user_input(\n    ctx,\n    name,\n    tentacle,\n    config_name=None,\n    trigger_if_necessary=True,\n    include_tentacle_as_requirement=True,\n    config: dict = None\n):\n    triggered = False\n    try:\n        if config_name is None:\n            query = await ctx.run_data_writer.search()\n            raw_value = await ctx.run_data_writer.select(\n                commons_enums.DBTables.INPUTS.value,\n                (query.name == name) & (query.tentacle == tentacle)\n            )\n            if raw_value:\n                return raw_value[0][\"value\"]\n        else:\n            # look for the user input in non nested user inputs\n            user_inputs = await commons_configuration.get_user_inputs(ctx.run_data_writer)\n            # First try with the current top level tentacle (faster and to avoid name conflicts)\n            top_tentacle_config = ctx.top_level_tentacle.get_local_config()\n            tentacle_config = _find_configuration(top_tentacle_config,\n                                                  ctx.nested_config_names,\n                                                  config_name.replace(\" \", \"_\"))\n            if tentacle_config is None:\n                # Then try with the current local tentacle, then use all tentacles\n                current_tentacle_config = ctx.tentacle.get_local_config()\n                tentacle_config = current_tentacle_config.get(config_name.replace(\" \", \"_\"), None)\n            if tentacle_config is None:\n                for local_user_input in user_inputs:\n                    if not local_user_input[\"is_nested_config\"] and \\\n                       local_user_input[\"input_type\"] == commons_constants.NESTED_TENTACLE_CONFIG:\n                        tentacle_config = _find_configuration(local_user_input[\"value\"],\n                                                              ctx.nested_config_names,\n                                                              config_name.replace(\" \", \"_\"))\n                        if tentacle_config is not None:\n                            break\n                if not trigger_if_necessary:\n                    # look into nested config as well since the tentacle wont be triggered\n                    for local_user_input in user_inputs:\n                        if local_user_input[\"is_nested_config\"] and \\\n                                local_user_input[\"input_type\"] == commons_constants.NESTED_TENTACLE_CONFIG:\n                            if local_user_input[\"name\"] == config_name:\n                                tentacle_config = local_user_input[\"value\"]\n                                break\n                            tentacle_config = _find_configuration(local_user_input[\"value\"],\n                                                                  ctx.nested_config_names,\n                                                                  config_name.replace(\" \", \"_\"))\n                            if tentacle_config is not None:\n                                break\n            if tentacle_config is None and trigger_if_necessary:\n                tentacle_class = tentacles_manager_api.get_tentacle_class_from_string(tentacle) \\\n                    if isinstance(tentacle, str) else tentacle\n                _, tentacle_config = await eval_triggered._trigger_single_evaluation(\n                    ctx, tentacle_class,\n                    commons_enums.CacheDatabaseColumns.VALUE.value,\n                    None,\n                    config_name, config)\n                triggered = True\n            try:\n                return None if tentacle_config is None else tentacle_config[name.replace(\" \", \"_\")]\n            except KeyError:\n                return None\n    finally:\n        if include_tentacle_as_requirement and not triggered and trigger_if_necessary:\n            # to register the tentacle as requirement: trigger its evaluation in a nested context\n            tentacle_class = tentacles_manager_api.get_tentacle_class_from_string(tentacle) \\\n                if isinstance(tentacle, str) else tentacle\n            await eval_triggered._trigger_single_evaluation(\n                ctx, tentacle_class,\n                commons_enums.CacheDatabaseColumns.VALUE.value,\n                None,\n                config_name, config)\n    return None\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/inputs/select_candle.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_public_data as exchange_public_data\n\n\nasync def user_select_candle(\n        ctx,\n        name=\"Select Candle Source\",\n        def_val=\"close\",\n        time_frame=None,\n        symbol=None,\n        limit=-1,\n        enable_volume=True,\n        return_source_name=False,\n        max_history=False,\n        show_in_summary=True,\n        show_in_optimizer=True,\n        order=None,\n):\n    available_data_src = [\"open\", \"high\", \"low\", \"close\", \"hl2\", \"hlc3\", \"ohlc4\",\n                          \"Heikin Ashi open\", \"Heikin Ashi high\", \"Heikin Ashi low\", \"Heikin Ashi close\"]\n    if enable_volume:\n        available_data_src.append(\"volume\")\n\n    data_source = await basic_keywords.user_input(ctx, name, \"options\", def_val, options=available_data_src,\n                                                  show_in_summary=show_in_summary, show_in_optimizer=show_in_optimizer,\n                                                  order=order)\n    candle_source = await exchange_public_data.get_candles_from_name(\n        ctx, source_name=data_source, time_frame=time_frame, symbol=symbol, limit=limit, max_history=max_history\n    )\n    if return_source_name:\n        return candle_source, data_source\n    else:\n        return candle_source\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/inputs/select_history.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\nimport octobot_trading.constants as trading_constants\n\n\nasync def set_candles_history_size(\n        ctx,\n        def_val=trading_constants.DEFAULT_CANDLE_HISTORY_SIZE,\n        name=trading_constants.CONFIG_CANDLES_HISTORY_SIZE_TITLE,\n        show_in_summary=False,\n        show_in_optimizer=False,\n        order=999,\n):\n    return await basic_keywords.user_input(ctx, name, \"int\", def_val,\n                                           show_in_summary=show_in_summary, show_in_optimizer=show_in_optimizer,\n                                           order=order)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/inputs/select_time_frame.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.errors as commons_errors\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.matrix as matrix\n\n\nasync def user_select_time_frame(\n        ctx,\n        def_val=\"1h\",\n        name=\"Timeframe\",\n        show_in_summary=True,\n        show_in_optimizer=True,\n        order=None\n):\n    available_timeframes = time_frame_manager.sort_time_frames(ctx.exchange_manager.client_time_frames)\n    selected_timeframe = await basic_keywords.user_input(ctx, name, \"options\", def_val, options=available_timeframes,\n                                                         show_in_summary=show_in_summary,\n                                                         show_in_optimizer=show_in_optimizer, order=order)\n    return selected_timeframe\n\n\nasync def user_multi_select_time_frame(\n        ctx,\n        def_val=\"1h\",\n        name=\"Timeframe\",\n        show_in_summary=True,\n        show_in_optimizer=True,\n        order=None\n):\n    available_timeframes = time_frame_manager.sort_time_frames(ctx.exchange_manager.client_time_frames)\n    selected_timeframe = await basic_keywords.user_input(ctx, name, \"multiple-options\", def_val,\n                                                         options=available_timeframes, show_in_summary=show_in_summary,\n                                                         show_in_optimizer=show_in_optimizer, order=order)\n    return selected_timeframe\n\n\nasync def set_trigger_time_frames(\n        ctx,\n        def_val=None,\n        show_in_summary=True,\n        show_in_optimizer=False,\n        order=None\n):\n    available_timeframes = [\n        tf.value\n        for tf in time_frame_manager.sort_time_frames(\n            ctx.exchange_manager.exchange_config.get_relevant_time_frames()\n        )\n    ]\n    def_val = def_val or available_timeframes[0]\n    name = commons_constants.CONFIG_TRIGGER_TIMEFRAMES.replace(\"_\", \" \")\n    trigger_timeframes = await basic_keywords.user_input(ctx, name, \"multiple-options\", def_val,\n                                                         options=available_timeframes, show_in_summary=show_in_summary,\n                                                         show_in_optimizer=show_in_optimizer, flush_if_necessary=True,\n                                                         order=order)\n    if ctx.time_frame not in trigger_timeframes:\n        if isinstance(ctx.tentacle, evaluators.AbstractEvaluator):\n            # For evaluators, make sure that undesired time frames are not in matrix anymore.\n            # Otherwise a strategy might wait for their value before pushing its evaluation to trading modes\n            matrix.delete_tentacle_node(\n                matrix_id=ctx.tentacle.matrix_id,\n                tentacle_path=matrix.get_matrix_default_value_path(\n                    exchange_name=ctx.exchange_manager.exchange_name,\n                    tentacle_type=ctx.tentacle.evaluator_type.value,\n                    tentacle_name=ctx.tentacle.get_name(),\n                    cryptocurrency=ctx.cryptocurrency,\n                    symbol=ctx.symbol,\n                    time_frame=ctx.time_frame if ctx.time_frame else None\n                )\n            )\n        raise commons_errors.ExecutionAborted(f\"Execution aborted: disallowed time frame: {ctx.time_frame}\")\n    return trigger_timeframes\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/inputs/triggers.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\n\n\nTRIGGER_ONLY_ON_THE_FIRST_CANDLE_KEY = \"trigger_only_on_the_first_candle\"\n\n\nasync def trigger_only_on_the_first_candle(ctx,\n                                           default_value,\n                                           show_in_summary=False,\n                                           show_in_optimizer=False,\n                                           order=700):\n    return await basic_keywords.user_input(ctx, TRIGGER_ONLY_ON_THE_FIRST_CANDLE_KEY, \"boolean\", default_value,\n                                           show_in_summary=show_in_summary,\n                                           show_in_optimizer=show_in_optimizer,\n                                           order=order)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/plots/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .displayed_elements import DisplayedElements\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/UI/plots/displayed_elements.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.errors as commons_errors\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.databases as databases\nimport octobot_commons.display as display\nimport octobot_backtesting.api as backtesting_api\nimport octobot_trading.api as trading_api\n\n\nclass DisplayedElements(display.DisplayTranslator):\n    TABLE_KEY_TO_COLUMN = {\n        commons_enums.PlotAttributes.X.value: \"Time\",\n        commons_enums.PlotAttributes.Y.value: \"Value\",\n        commons_enums.PlotAttributes.Z.value: \"Value\",\n        commons_enums.PlotAttributes.OPEN.value: \"Open\",\n        commons_enums.PlotAttributes.HIGH.value: \"High\",\n        commons_enums.PlotAttributes.LOW.value: \"Low\",\n        commons_enums.PlotAttributes.CLOSE.value: \"Close\",\n        commons_enums.PlotAttributes.VOLUME.value: \"Volume\",\n        commons_enums.DBRows.SYMBOL.value: \"Symbol\",\n    }\n\n    async def fill_from_database(self, trading_mode, database_manager, exchange_name, symbol, time_frame, exchange_id,\n                                 with_inputs=True, symbols=None, time_frames=None):\n        async with databases.MetaDatabase.database(database_manager) as meta_db:\n            graphs_by_parts = {}\n            inputs = []\n            candles = []\n            cached_values = []\n            if trading_mode.is_backtestable():\n                exchange_name, symbol, time_frame = \\\n                    await self._adapt_inputs_for_backtesting_results(meta_db, exchange_name, symbol, time_frame)\n            run_db = meta_db.get_run_db()\n            metadata_rows = await run_db.all(commons_enums.DBTables.METADATA.value)\n            metadata = metadata_rows[0] if metadata_rows else None\n            if symbols is not None:\n                symbols.extend(metadata[commons_enums.BacktestingMetadata.SYMBOLS.value])\n            if time_frames is not None:\n                time_frames.extend(metadata[commons_enums.BacktestingMetadata.TIME_FRAMES.value])\n            account_type = trading_api.get_account_type_from_run_metadata(metadata) \\\n                if database_manager.is_backtesting() \\\n                else trading_api.get_account_type_from_exchange_manager(\n                trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n            )\n            dbs = [\n                run_db,\n                meta_db.get_transactions_db(account_type, exchange_name),\n                meta_db.get_orders_db(account_type, exchange_name),\n                meta_db.get_trades_db(account_type, exchange_name),\n                meta_db.get_symbol_db(exchange_name, symbol)\n            ]\n            for index, db in enumerate(dbs):\n                for table_name in await db.tables():\n                    display_data = await db.all(table_name)\n                    if table_name == commons_enums.DBTables.INPUTS.value:\n                        inputs += display_data\n                    if table_name == commons_enums.DBTables.CANDLES_SOURCE.value:\n                        candles += display_data\n                    if table_name == commons_enums.DBTables.CACHE_SOURCE.value:\n                        cached_values += display_data\n                    else:\n                        try:\n                            filter_symbol = index != len(dbs) - 1   # don't filter symbol for symbol db\n                            filtered_data = self._filter_and_adapt_displayed_elements(\n                                display_data, symbol, time_frame, table_name, filter_symbol\n                            )\n                            chart = display_data[0][commons_enums.DisplayedElementTypes.CHART.value]\n                            if chart is None:\n                                continue\n                            if chart in graphs_by_parts:\n                                graphs_by_parts[chart][table_name] = filtered_data\n                            else:\n                                graphs_by_parts[chart] = {table_name: filtered_data}\n                        except (IndexError, KeyError):\n                            # some table have no chart\n                            pass\n            try:\n                run_start_time, run_end_time = await self._get_run_window(meta_db.get_run_db())\n            except IndexError:\n                run_start_time = run_end_time = 0\n            first_candle_time, last_candle_time = \\\n                await self._add_candles(graphs_by_parts, candles, exchange_name, exchange_id, symbol, time_frame,\n                                        run_start_time, run_end_time)\n            await self._add_cached_values(graphs_by_parts, cached_values, time_frame,\n                                          first_candle_time, last_candle_time)\n            self._plot_graphs(graphs_by_parts)\n            if with_inputs:\n                with self.part(commons_enums.DBTables.INPUTS.value,\n                               element_type=commons_enums.DisplayedElementTypes.INPUT.value) as part:\n                    self.add_user_inputs(inputs, part)\n\n    async def _adapt_inputs_for_backtesting_results(self, meta_db, exchange_name, symbol, time_frame):\n        if not await meta_db.run_dbs_identifier.exchange_base_identifier_exists(exchange_name):\n            single_exchange = await meta_db.run_dbs_identifier.get_single_existing_exchange()\n            if single_exchange is None:\n                # no single exchange with data\n                raise commons_errors.MissingExchangeDataError(\n                    f\"No data for {exchange_name}. This run might have happened on other exchange(s)\"\n                )\n            else:\n                # retarget exchange_name\n                exchange_name = single_exchange\n        if not await meta_db.run_dbs_identifier.symbol_base_identifier_exists(exchange_name, symbol):\n            run_metadata = await meta_db.get_run_db().all(commons_enums.DBTables.METADATA.value)\n            try:\n                symbols = run_metadata[0].get(commons_enums.DBRows.SYMBOLS.value, [])\n                if len(symbols) > 0:\n                    # retarget symbol\n                    symbol = symbols[0]\n                else:\n                    # no single exchange with data\n                    raise commons_errors.MissingExchangeDataError(\n                        f\"No symbol related data for {exchange_name}\"\n                    )\n            except IndexError:\n                # no run metadata, try to continue\n                pass\n        return exchange_name, symbol, time_frame\n\n    def _plot_graphs(self, graphs_by_parts):\n        for part, datasets in graphs_by_parts.items():\n            with self.part(part, element_type=commons_enums.DisplayedElementTypes.CHART.value) as part:\n                for title, dataset in datasets.items():\n                    if not dataset:\n                        continue\n                    x = []\n                    y = []\n                    open = []\n                    high = []\n                    low = []\n                    close = []\n                    volume = []\n                    text = []\n                    color = []\n                    size = []\n                    shape = []\n                    if dataset[0].get(commons_enums.PlotAttributes.X.value, None) is None:\n                        x = None\n                    if dataset[0].get(commons_enums.PlotAttributes.Y.value, None) is None:\n                        y = None\n                    if dataset[0].get(commons_enums.PlotAttributes.OPEN.value, None) is None:\n                        open = None\n                    if dataset[0].get(commons_enums.PlotAttributes.HIGH.value, None) is None:\n                        high = None\n                    if dataset[0].get(commons_enums.PlotAttributes.LOW.value, None) is None:\n                        low = None\n                    if dataset[0].get(commons_enums.PlotAttributes.CLOSE.value, None) is None:\n                        close = None\n                    if dataset[0].get(commons_enums.PlotAttributes.VOLUME.value, None) is None:\n                        volume = None\n                    if dataset[0].get(commons_enums.PlotAttributes.TEXT.value, None) is None:\n                        text = None\n                    if dataset[0].get(commons_enums.PlotAttributes.COLOR.value, None) is None:\n                        color = None\n                    if dataset[0].get(commons_enums.PlotAttributes.SIZE.value, None) is None:\n                        size = None\n                    if dataset[0].get(commons_enums.PlotAttributes.SHAPE.value, None) is None:\n                        shape = None\n                    own_yaxis = dataset[0].get(commons_enums.PlotAttributes.OWN_YAXIS.value, False)\n                    for data in dataset:\n                        if x is not None:\n                            x.append(data[commons_enums.PlotAttributes.X.value])\n                        if y is not None:\n                            y.append(data[commons_enums.PlotAttributes.Y.value])\n                        if open is not None:\n                            open.append(data[commons_enums.PlotAttributes.OPEN.value])\n                        if high is not None:\n                            high.append(data[commons_enums.PlotAttributes.HIGH.value])\n                        if low is not None:\n                            low.append(data[commons_enums.PlotAttributes.LOW.value])\n                        if close is not None:\n                            close.append(data[commons_enums.PlotAttributes.CLOSE.value])\n                        if volume is not None:\n                            volume.append(data[commons_enums.PlotAttributes.VOLUME.value])\n                        if text is not None:\n                            text.append(data[commons_enums.PlotAttributes.TEXT.value])\n                        if color is not None:\n                            color.append(data[commons_enums.PlotAttributes.COLOR.value])\n                        if size is not None:\n                            size.append(data[commons_enums.PlotAttributes.SIZE.value])\n                        if shape is not None:\n                            shape.append(data[commons_enums.PlotAttributes.SHAPE.value])\n                    # use log scale for all positive charts\n                    y_type = None\n                    if title == commons_enums.DBTables.CANDLES_SOURCE.value \\\n                            or 0 <= min(d.get(commons_enums.PlotAttributes.Y.value, 0) for d in dataset):\n                        y_type = \"log\"\n\n                    part.plot(\n                        kind=data.get(commons_enums.PlotAttributes.KIND.value, None),\n                        x=x,\n                        y=y,\n                        open=open,\n                        high=high,\n                        low=low,\n                        close=close,\n                        volume=volume,\n                        title=title,\n                        text=text,\n                        x_type=\"date\",\n                        y_type=y_type,\n                        mode=data.get(commons_enums.PlotAttributes.MODE.value, None),\n                        own_yaxis=own_yaxis,\n                        color=color,\n                        size=size,\n                        symbol=shape)\n\n    def _adapt_for_display(self, table_name, filtered_elements):\n        if table_name == commons_enums.DBTables.TRANSACTIONS.value:\n            # only display liquidations\n            filtered_elements = [\n                display_element\n                for display_element in filtered_elements\n                if display_element.get(\"trigger_source\", None) == trading_enums.PNLTransactionSource.LIQUIDATION.value\n            ]\n            for display_element in filtered_elements:\n                display_element[commons_enums.PlotAttributes.COLOR.value] = \"red\"\n                display_element[commons_enums.PlotAttributes.SHAPE.value] = commons_enums.PlotAttributes.X.value\n                display_element[commons_enums.PlotAttributes.SIZE.value] = 15\n                display_element[commons_enums.PlotAttributes.TEXT.value] = f\"Liquidation ({abs(display_element.get('closed_quantity', 0))} liquidated)\"\n                display_element[commons_enums.PlotAttributes.Y.value] = display_element[\"order_exit_price\"]\n        elif table_name == commons_enums.DBTables.ORDERS.value:\n            # adapt order details for display\n            for display_element in filtered_elements:\n                order_details = display_element[trading_constants.STORAGE_ORIGIN_VALUE]\n                side = order_details[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]\n                display_element[commons_enums.PlotAttributes.COLOR.value] = \"red\" \\\n                    if side == trading_enums.TradeOrderSide.SELL.value else \"green\"\n                display_element[commons_enums.PlotAttributes.SHAPE.value] = \"line-ew-open\"\n                display_element[commons_enums.PlotAttributes.MODE.value] = \"markers\"\n                display_element[commons_enums.PlotAttributes.KIND.value] = \"scattergl\"\n                display_element[commons_enums.PlotAttributes.SIZE.value] = 15\n                display_element[commons_enums.PlotAttributes.TEXT.value] = \\\n                    f\"{order_details[trading_enums.ExchangeConstantsOrderColumns.TYPE.value]} \" \\\n                    f\"{order_details[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value]} \" \\\n                    f\"{order_details[trading_enums.ExchangeConstantsOrderColumns.QUANTITY_CURRENCY.value]} \" \\\n                    f\"at {order_details[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]}\"\n                display_element[commons_enums.PlotAttributes.Y.value] = \\\n                    order_details[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]\n                display_element[commons_enums.PlotAttributes.X.value] = \\\n                    order_details[trading_enums.ExchangeConstantsOrderColumns.TIMESTAMP.value] * 1000\n                display_element[commons_enums.DisplayedElementTypes.CHART.value] = \\\n                    commons_enums.PlotCharts.MAIN_CHART.value\n        return filtered_elements\n\n    def _filter_and_adapt_displayed_elements(self, elements, symbol, time_frame, table_name, filter_symbol):\n        default_symbol = None if filter_symbol else symbol\n        filtered_elements = [\n            display_element\n            for display_element in elements\n            if (\n                display_element.get(commons_enums.DBRows.SYMBOL.value, default_symbol) == symbol\n                and display_element.get(commons_enums.DBRows.TIME_FRAME.value) == time_frame\n            ) or (\n                display_element.get(trading_constants.STORAGE_ORIGIN_VALUE, {})\n                .get(trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value, default_symbol) == symbol\n            )\n        ]\n        return self._adapt_for_display(table_name, filtered_elements)\n\n    async def _get_run_window(self, run_database):\n        run_metadata = (await run_database.all(commons_enums.DBTables.METADATA.value))[0]\n        end_time = run_metadata.get(\"end_time\", 0)\n        if end_time == -1:\n            # live mode\n            return 0, 0\n        return run_metadata.get(\"start_time\", 0), end_time\n\n    async def _add_cached_values(self, graphs_by_parts, cached_values, time_frame, start_time, end_time):\n        start_time = start_time\n        end_time = end_time\n        for cached_value_metadata in cached_values:\n            if cached_value_metadata.get(commons_enums.DBRows.TIME_FRAME.value, None) == time_frame:\n                try:\n                    chart = cached_value_metadata[commons_enums.DisplayedElementTypes.CHART.value]\n                    x_shift = cached_value_metadata[\"x_shift\"]\n                    values = sorted(await self._get_cached_values_to_display(cached_value_metadata, x_shift,\n                                                                             start_time, end_time),\n                                    key=lambda x: x[commons_enums.PlotAttributes.X.value])\n                    try:\n                        graphs_by_parts[chart][cached_value_metadata[commons_enums.PlotAttributes.TITLE.value]] = values\n                    except KeyError:\n                        if chart not in graphs_by_parts:\n                            graphs_by_parts[chart] = {}\n                        try:\n                            graphs_by_parts[chart] = \\\n                                {cached_value_metadata[commons_enums.PlotAttributes.TITLE.value]: values}\n                        except KeyError:\n                            graphs_by_parts[chart] = {commons_enums.PlotAttributes.TITLE.value: values}\n                except KeyError:\n                    # some table have no chart\n                    pass\n\n    async def _get_cached_values_to_display(self, cached_value_metadata, x_shift, start_time, end_time):\n        cache_file = cached_value_metadata[commons_enums.PlotAttributes.VALUE.value]\n        cache_displayed_value = plotted_displayed_value = cached_value_metadata[\"cache_value\"]\n        kind = cached_value_metadata[commons_enums.PlotAttributes.KIND.value]\n        mode = cached_value_metadata[commons_enums.PlotAttributes.MODE.value]\n        own_yaxis = cached_value_metadata[commons_enums.PlotAttributes.OWN_YAXIS.value]\n        condition = cached_value_metadata.get(\"condition\", None)\n        try:\n            cache_database = databases.CacheDatabase(cache_file)\n            cache_type = (await cache_database.get_metadata())[commons_enums.CacheDatabaseColumns.TYPE.value]\n            if cache_type == databases.CacheTimestampDatabase.__name__:\n                cache = await cache_database.get_cache()\n                for cache_val in cache:\n                    try:\n                        if isinstance(cache_val[cache_displayed_value], bool):\n                            plotted_displayed_value = self._get_cache_displayed_value(cache_val, cache_displayed_value)\n                            if plotted_displayed_value is None:\n                                self.logger.error(f\"Impossible to plot {cache_displayed_value}: unset y axis value\")\n                                return []\n                        else:\n                            break\n                    except KeyError:\n                        pass\n                    except Exception as e:\n                        print(e)\n                plotted_values = []\n                for values in cache:\n                    try:\n                        if condition is None or condition == values[cache_displayed_value]:\n                            x = (values[commons_enums.CacheDatabaseColumns.TIMESTAMP.value] + x_shift) * 1000\n                            if (start_time == end_time == 0) or start_time <= x <= end_time:\n                                y = values[plotted_displayed_value]\n                                if not isinstance(x, list) and isinstance(y, list):\n                                    for y_val in y:\n                                        plotted_values.append({\n                                            commons_enums.PlotAttributes.X.value: x,\n                                            commons_enums.PlotAttributes.Y.value: y_val,\n                                            commons_enums.PlotAttributes.KIND.value: kind,\n                                            commons_enums.PlotAttributes.MODE.value: mode,\n                                            commons_enums.PlotAttributes.OWN_YAXIS.value: own_yaxis,\n                                        })\n                                else:\n                                    plotted_values.append({\n                                        commons_enums.PlotAttributes.X.value: x,\n                                        commons_enums.PlotAttributes.Y.value: y,\n                                        commons_enums.PlotAttributes.KIND.value: kind,\n                                        commons_enums.PlotAttributes.MODE.value: mode,\n                                        commons_enums.PlotAttributes.OWN_YAXIS.value: own_yaxis,\n                                    })\n                    except KeyError:\n                        pass\n                return plotted_values\n            self.logger.error(f\"Unhandled cache type to display: {cache_type}\")\n        except TypeError:\n            self.logger.error(f\"Missing cache type in {cache_file} metadata file\")\n        except commons_errors.DatabaseNotFoundError as ex:\n            self.logger.warning(f\"Missing cache values ({ex})\")\n        return []\n\n    @staticmethod\n    def _get_cache_displayed_value(cache_val, base_displayed_value):\n        for key in cache_val.keys():\n            separator_split_key = key.split(commons_constants.CACHE_RELATED_DATA_SEPARATOR)\n            if base_displayed_value == separator_split_key[0] and len(separator_split_key) == 2:\n                return key\n        return None\n\n    async def _add_candles(self, graphs_by_parts, candles_list, exchange_name, exchange_id, symbol, time_frame,\n                           run_start_time, run_end_time):\n        first_candle_time = last_candle_time = 0\n        for candles_metadata in candles_list:\n            if candles_metadata.get(commons_enums.DBRows.TIME_FRAME.value) == time_frame:\n                try:\n                    chart = candles_metadata[commons_enums.DisplayedElementTypes.CHART.value]\n                    candles = await self._get_candles_to_display(candles_metadata, exchange_name,\n                                                                 exchange_id, symbol, time_frame,\n                                                                 run_start_time, run_end_time)\n                    try:\n                        graphs_by_parts[chart][commons_enums.DBTables.CANDLES.value] = candles\n                    except KeyError:\n                        graphs_by_parts[chart] = {commons_enums.DBTables.CANDLES.value: candles}\n                    # candles are assumed to be ordered\n                    if first_candle_time == 0 or first_candle_time < candles[0][commons_enums.PlotAttributes.X.value]:\n                        first_candle_time = candles[0][commons_enums.PlotAttributes.X.value]\n                    if last_candle_time == 0 or last_candle_time > candles[-1][commons_enums.PlotAttributes.X.value]:\n                        last_candle_time = candles[-1][commons_enums.PlotAttributes.X.value]\n                except KeyError:\n                    # some table have no chart\n                    pass\n        return first_candle_time, last_candle_time\n\n    async def _get_candles_to_display(self, candles_metadata, exchange_name, exchange_id, symbol, time_frame,\n                                      run_start_time, run_end_time):\n        if candles_metadata[commons_enums.DBRows.VALUE.value] == commons_constants.LOCAL_BOT_DATA:\n            exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n            array_candles = trading_api.get_symbol_historical_candles(\n                trading_api.get_symbol_data(exchange_manager, symbol, allow_creation=False), time_frame\n            )\n            return [\n                {\n                    commons_enums.PlotAttributes.X.value: time * 1000,\n                    commons_enums.PlotAttributes.OPEN.value: array_candles[commons_enums.PriceIndexes.IND_PRICE_OPEN.value][index],\n                    commons_enums.PlotAttributes.HIGH.value: array_candles[commons_enums.PriceIndexes.IND_PRICE_HIGH.value][index],\n                    commons_enums.PlotAttributes.LOW.value: array_candles[commons_enums.PriceIndexes.IND_PRICE_LOW.value][index],\n                    commons_enums.PlotAttributes.CLOSE.value: array_candles[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value][index],\n                    commons_enums.PlotAttributes.VOLUME.value: array_candles[commons_enums.PriceIndexes.IND_PRICE_VOL.value][index],\n                    commons_enums.PlotAttributes.KIND.value: \"candlestick\",\n                    commons_enums.PlotAttributes.MODE.value: \"lines\",\n                }\n                for index, time in enumerate(array_candles[commons_enums.PriceIndexes.IND_PRICE_TIME.value])\n                if (run_start_time == run_end_time == 0) or run_start_time <= time <= run_end_time\n            ]\n        db_candles = await backtesting_api.get_all_ohlcvs(candles_metadata[commons_enums.DBRows.VALUE.value],\n                                                          exchange_name,\n                                                          symbol,\n                                                          commons_enums.TimeFrames(time_frame),\n                                                          inferior_timestamp=run_start_time if run_start_time > 0\n                                                          else -1,\n                                                          superior_timestamp=run_end_time if run_end_time > 0 else -1)\n        return [\n            {\n                commons_enums.PlotAttributes.X.value: db_candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] * 1000,\n                commons_enums.PlotAttributes.OPEN.value: db_candle[commons_enums.PriceIndexes.IND_PRICE_OPEN.value],\n                commons_enums.PlotAttributes.HIGH.value: db_candle[commons_enums.PriceIndexes.IND_PRICE_HIGH.value],\n                commons_enums.PlotAttributes.LOW.value: db_candle[commons_enums.PriceIndexes.IND_PRICE_LOW.value],\n                commons_enums.PlotAttributes.CLOSE.value: db_candle[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value],\n                commons_enums.PlotAttributes.VOLUME.value: db_candle[commons_enums.PriceIndexes.IND_PRICE_VOL.value],\n                commons_enums.PlotAttributes.KIND.value: \"candlestick\",\n                commons_enums.PlotAttributes.MODE.value: \"lines\",\n            }\n            for index, db_candle in enumerate(db_candles)\n        ]\n\n    def plot(\n            self,\n            x,\n            y=None,\n            open=None,\n            high=None,\n            low=None,\n            close=None,\n            volume=None,\n            x_type=\"date\",\n            y_type=None,\n            title=None,\n            text=None,\n            kind=\"scattergl\",\n            mode=\"lines\",\n            line_shape=\"linear\",\n            own_xaxis=False,\n            own_yaxis=False,\n            color=None,\n            size=None,\n            symbol=None,\n    ):\n        element = display.Element(\n            kind,\n            x,\n            y,\n            open=open,\n            high=high,\n            low=low,\n            close=close,\n            volume=volume,\n            x_type=x_type,\n            y_type=y_type,\n            title=title,\n            text=text,\n            mode=mode,\n            line_shape=line_shape,\n            own_xaxis=own_xaxis,\n            own_yaxis=own_yaxis,\n            type=commons_enums.DisplayedElementTypes.CHART.value,\n            color=color,\n            size=size,\n            symbol=symbol\n        )\n        self.elements.append(element)\n\n    def table(\n            self,\n            name,\n            columns,\n            rows,\n            searches\n    ):\n        element = display.Element(\n            None,\n            None,\n            None,\n            title=name,\n            columns=columns,\n            rows=rows,\n            searches=searches,\n            type=commons_enums.DisplayedElementTypes.TABLE.value\n        )\n        self.elements.append(element)\n\n    def value(self, label, value):\n        element = display.Element(\n            None,\n            None,\n            None,\n            title=label,\n            value=str(value),\n            type=commons_enums.DisplayedElementTypes.VALUE.value\n        )\n        self.elements.append(element)\n\n    def html_value(self, html):\n        element = display.Element(\n            None,\n            None,\n            None,\n            html=html,\n            type=commons_enums.DisplayedElementTypes.VALUE.value\n        )\n        self.elements.append(element)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .data import *\nfrom .UI import *\nfrom .orders import *\nfrom .TA import *\nfrom .settings import *\nfrom .backtesting import *\nfrom .alerts import *\nfrom .configuration import *\nfrom .exchanges import *\n\n# shortcut to octobot-trading keywords\nfrom octobot_trading.modes.script_keywords.basic_keywords import *\nfrom octobot_trading.modes.script_keywords.dsl import *\nfrom octobot_trading.modes.script_keywords.context_management import Context\nfrom octobot_trading.enums import *\nfrom octobot_commons.enums import BacktestingMetadata, DBTables, DBRows\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/alerts/__init__.py",
    "content": "from .notifications import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/alerts/notifications.py",
    "content": "import octobot_services.api as services_api\nimport octobot_services.enums as services_enum\n\n\nasync def send_alert(title, alert_content,\n                     level: services_enum.NotificationLevel = services_enum.NotificationLevel.INFO,\n                     sound=services_enum.NotificationSound.NO_SOUND):\n    await services_api.send_notification(services_api.create_notification(alert_content, title=title, level=level,\n                                                                          markdown_text=alert_content,\n                                                                          sound=sound,\n                                                                          category=services_enum.\n                                                                          NotificationCategory.TRADING_SCRIPT_ALERTS))\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .metadata import *\nfrom .run_data_analysis import *\nfrom .backtesting_data_selector import *\nfrom .backtesting_settings import *\nfrom .default_backtesting_run_analysis_script import *\nfrom .backtesting_intialization import *\nfrom .backtesting_data_collector import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/backtesting_data_collector.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport contextlib\nimport time\nimport typing\nimport datetime\n\nimport octobot_commons\nimport octobot_commons.constants as common_constants\nimport octobot_commons.enums as common_enums\nimport octobot_commons.profiles as commons_profiles\nimport octobot_commons.timestamp_util as timestamp_util\nimport octobot_commons.symbols\nimport octobot_commons.logging\n\nimport octobot_trading.exchanges\nimport octobot_trading.util.test_tools.exchange_data as exchange_data_import\nimport octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools\n\nimport octobot.community\nimport octobot.enums\nimport octobot.constants as constants\n\nimport tentacles.Meta.Keywords.scripting_library.errors as scr_errors\nimport tentacles.Meta.Keywords.scripting_library.configuration as scr_configuration\nimport tentacles.Meta.Keywords.scripting_library.exchanges as src_exchanges\nimport tentacles.Meta.Keywords.scripting_library.constants as scr_constants\n\nimport tentacles.Meta.Keywords.scripting_library.errors as errors\n\n\nasync def init_exchange_market_status_and_populate_backtesting_exchange_data(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n    backend_type: typing.Optional[octobot.enums.CommunityHistoricalBackendType] = None,\n) -> exchange_data_import.ExchangeData:\n    \"\"\"\n    Initializes the exchange market status and populates the backtesting exchange data.\n    If a backend type is provided, it will use the historical client to populate the backtesting exchange data.\n    Otherwise, it will use the ccxt exchange manager to populate the backtesting exchange data.\n    \"\"\"\n    async with data_collector_ccxt_exchange_manager(\n        profile_data, exchange_data\n    ) as exchange_manager:\n        if backend_type is not None:\n            async with octobot.community.history_backend_client(\n                backend_type=backend_type\n            ) as historical_client:\n                return await populate_backtesting_exchange_data_from_historical_client(\n                    exchange_data, profile_data, historical_client, exchange_manager.exchange_name\n                )\n        return await fetch_and_populate_backtesting_exchange_data(\n            exchange_data, profile_data, exchange_manager\n        )\n\n\nasync def fetch_and_populate_backtesting_exchange_data(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n    exchange_manager: octobot_trading.exchanges.ExchangeManager,\n) -> exchange_data_import.ExchangeData:\n    start_time, end_time, time_frames, symbols = _get_backtesting_run_details(profile_data)\n    for time_frame in time_frames:\n        await exchanges_test_tools.add_symbols_details(\n            exchange_manager, symbols, time_frame.value, exchange_data,\n            start_time=start_time, end_time=end_time,\n            close_price_only=False,\n            include_latest_candle=False,\n        )\n    first_candle_times = []\n    for market in exchange_data.markets:\n        first_candle_times.append(market.time[0])\n    _ensure_start_time(exchange_data, start_time, first_candle_times)\n    return exchange_data\n\n\ndef _get_backtesting_run_details(\n    profile_data: commons_profiles.ProfileData,\n) -> (float, float, list[common_enums.TimeFrames], list[str]):\n    start_time = get_backtesting_start_time(profile_data)\n    end_time = time.time()\n    time_frames = [\n        common_enums.TimeFrames(tf)\n        for tf in scr_configuration.get_time_frames(profile_data, for_historical_data=True)\n    ]\n    if (\n        scr_configuration.requires_price_update_timeframe(profile_data)\n        and scr_constants.PRICE_UPDATE_TIME_FRAME.value not in time_frames\n    ):\n        time_frames.append(scr_constants.PRICE_UPDATE_TIME_FRAME)\n    symbols = scr_configuration.get_traded_symbols(profile_data)\n    return start_time, end_time, time_frames, symbols\n\n\ndef get_backtesting_start_time(\n    profile_data: commons_profiles.ProfileData\n) -> float:\n    return time.time() - profile_data.backtesting_context.start_time_delta\n\n\ndef iter_fetched_ohlcvs(ohlcvs: list[list[typing.Union[float, str]]]):\n    ohlcvs_by_symbol = {}\n    for ohlcv in ohlcvs:\n        time_frame = ohlcv[0]\n        symbol = ohlcv[1]\n        candles = ohlcv[2:]\n        if symbol not in ohlcvs_by_symbol:\n            ohlcvs_by_symbol[symbol] = {}\n        if time_frame not in ohlcvs_by_symbol[symbol]:\n            ohlcvs_by_symbol[symbol][time_frame] = []\n        ohlcvs_by_symbol[symbol][time_frame].append(candles)\n    for symbol, time_frames in ohlcvs_by_symbol.items():\n        for time_frame, ohlcvs in time_frames.items():\n            yield symbol, time_frame, ohlcvs\n\n\nasync def populate_backtesting_exchange_data_from_historical_client(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n    historical_client: octobot.community.HistoricalBackendClient,\n    exchange_name: str\n) -> exchange_data_import.ExchangeData:\n    start_time, end_time, time_frames, symbols = _get_backtesting_run_details(profile_data)\n    first_traded_symbols, last_traded_symbols, first_historical_config_time = (\n        scr_configuration.get_oldest_historical_config_symbols_and_time(profile_data, start_time)\n    )\n    exchange_data.exchange_details.name = profile_data.backtesting_context.exchanges[0]   # todo handle multi exchanges\n    scr_configuration.set_backtesting_portfolio(profile_data, exchange_data)\n    exchange_data, updated_start_time = await update_backtesting_symbols_data(\n        historical_client, profile_data, exchange_name, symbols, time_frames, exchange_data, start_time, end_time,\n        first_traded_symbols, last_traded_symbols, first_historical_config_time\n    )\n    if not scr_configuration.can_convert_ref_market_to_usd_like(exchange_data, profile_data):\n        # usd like convert\n        try:\n            usd_like_time_frame = time_frames[0]\n            symbol = await find_usd_like_symbol_from_available_history(\n                historical_client, exchange_data.exchange_details.name,\n                profile_data.trading.reference_market, usd_like_time_frame, updated_start_time, end_time,\n            )\n            await update_backtesting_symbols_data(\n                historical_client, profile_data, exchange_name, [symbol], [usd_like_time_frame],\n                exchange_data, updated_start_time, end_time, [symbol], [symbol],\n                first_historical_config_time, close_price_only=True,\n            )\n        except scr_errors.InvalidBacktestingDataError as err:\n            # can't convert ref market into usd like value\n            _get_logger().error(f\"Can't convert ref market into usd like value: {err}\")\n        except KeyError as err:\n            # can't convert ref market into usd like value\n            _get_logger().error(\n                f\"Can't convert ref market into usd like value: missing {err} timeframe values\"\n            )\n    return exchange_data\n\n\nasync def init_backtesting_exchange_market_status_cache(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n):\n    async with data_collector_ccxt_exchange_manager(profile_data, exchange_data):\n        # nothing to do, initializing the exchange manager is enough to fetch market statuses\n        pass\n\n\n@contextlib.asynccontextmanager\nasync def data_collector_ccxt_exchange_manager(\n    profile_data: commons_profiles.ProfileData,\n    exchange_data: exchange_data_import.ExchangeData,\n):\n    exchange_data.exchange_details.name = profile_data.backtesting_context.exchanges[0]\n    tentacles_setup_config = scr_configuration.get_full_tentacles_setup_config()\n    exchange_config_by_exchange = scr_configuration.get_config_by_tentacle(profile_data)\n    async with src_exchanges.local_ccxt_exchange_manager(\n        exchange_data, tentacles_setup_config,\n        exchange_config_by_exchange=exchange_config_by_exchange,\n    ) as exchange_manager:\n        try:\n            yield exchange_manager\n        except Exception as err:\n            _get_logger().exception(err)\n            raise\n\n\nasync def fetch_candles_history_range(\n    historical_client: octobot.community.HistoricalBackendClient,\n    exchange: str, symbol: str, time_frame: common_enums.TimeFrames\n) -> (float, float):\n    return await historical_client.fetch_candles_history_range(exchange, symbol, time_frame)\n\n\nasync def find_usd_like_symbol_from_available_history(\n    historical_client: octobot.community.HistoricalBackendClient,\n    exchange_name: str, base: str, time_frame: common_enums.TimeFrames,\n    first_open_time: float, last_open_time: float,\n) -> str:\n    for usd_like_coin in common_constants.USD_LIKE_COINS:\n        symbol = octobot_commons.symbols.merge_currencies(base, usd_like_coin)\n        first_candle_time, last_candle_time = await fetch_candles_history_range(\n            # always use production db\n            historical_client, exchange_name, symbol, time_frame\n        )\n        if not (last_candle_time and first_candle_time):\n            continue\n        try:\n            ensure_compatible_candle_time(\n                exchange_name, symbol, time_frame,\n                first_open_time, last_open_time, first_candle_time, last_candle_time,\n                True, True, True, first_open_time,\n                False\n            )\n            # did not raise: symbol can be used\n            return symbol\n        except scr_errors.InvalidBacktestingDataError:\n            # can't use this symbol, proceed to the next one\n            continue\n    raise scr_errors.InvalidBacktestingDataError(\n        f\"No USD-like up to date candles found to convert {base} into USD-like on {exchange_name} {time_frame.value} \"\n        f\"for first_open_time={first_open_time} last_open_time={last_open_time}\"\n    )\n\n\nasync def update_backtesting_symbols_data(\n    historical_client: octobot.community.HistoricalBackendClient,\n    profile_data: commons_profiles.ProfileData,\n    exchange_name: str, symbols: list, time_frames: list,\n    exchange_data: exchange_data_import.ExchangeData,\n    start_time: float, end_time: float,\n    first_traded_symbols: list, last_traded_symbols: list, first_traded_symbols_time: float,\n    close_price_only: bool = False,\n    requires_traded_symbol_prices_at_all_time: bool = True,\n) -> (exchange_data_import.ExchangeData, float):\n    updated_start_times = []\n    is_custom_strategy = octobot.community.models.is_custom_strategy_profile(profile_data)\n    # can adapt backtesting start and end time on custom strategies that require symbol prices at all time\n    allow_any_backtesting_start_and_end_time = is_custom_strategy and requires_traded_symbol_prices_at_all_time\n\n    all_ohlcvs = await historical_client.fetch_extended_candles_history(\n        exchange_name, symbols, time_frames, start_time, end_time\n    )\n\n    for symbol, str_time_frame, ohlcvs in iter_fetched_ohlcvs(all_ohlcvs):\n        time_frame = common_enums.TimeFrames(str_time_frame)\n        # do not take current incomplete candle into account\n        last_open_time = end_time - common_enums.TimeFramesMinutes[time_frame] * common_constants.MINUTE_TO_SECONDS\n        # When symbol in is first_traded_symbols, it should be available from the start\n        # EXCEPT for custom strategies that might require trading pairs that don't exist for long enough\n        # (when compatible with trading mode).\n        # Otherwise, when it is available doesn't really matter.\n        # If it's not available from the start, adapt start time to start as early as possible,\n        # latest being first_traded_symbols_time.\n        required_from_the_start = symbol in first_traded_symbols and (\n            requires_traded_symbol_prices_at_all_time or not is_custom_strategy\n        )\n        required_till_the_end = symbol in last_traded_symbols\n        updated_start_time = ensure_ohlcv_validity(\n            ohlcvs, exchange_name, symbol, time_frame, start_time, last_open_time,\n            required_from_the_start, required_till_the_end, first_traded_symbols_time,\n            allow_any_backtesting_start_and_end_time\n        )\n        if updated_start_time is not None:\n            updated_start_times.append(updated_start_time)\n        exchange_data.markets.append(exchange_data_import.MarketDetails(\n            symbol=symbol,\n            time_frame=time_frame.value,\n            close=[ohlcv[common_enums.PriceIndexes.IND_PRICE_CLOSE.value] for ohlcv in ohlcvs],\n            open=[ohlcv[common_enums.PriceIndexes.IND_PRICE_OPEN.value] for ohlcv in ohlcvs]\n            if not close_price_only else [],\n            high=[ohlcv[common_enums.PriceIndexes.IND_PRICE_HIGH.value] for ohlcv in ohlcvs]\n            if not close_price_only else [],\n            low=[ohlcv[common_enums.PriceIndexes.IND_PRICE_LOW.value] for ohlcv in ohlcvs]\n            if not close_price_only else [],\n            volume=[ohlcv[common_enums.PriceIndexes.IND_PRICE_VOL.value] for ohlcv in ohlcvs]\n            if not close_price_only else [],\n            time=[ohlcv[common_enums.PriceIndexes.IND_PRICE_TIME.value] for ohlcv in ohlcvs],\n        ))\n    updated_start_time = _ensure_start_time(\n        exchange_data, start_time, updated_start_times\n    )\n    return exchange_data, updated_start_time\n\n\ndef _ensure_start_time(\n    exchange_data: exchange_data_import.ExchangeData, ideal_start_time: float, updated_start_times: list[float]\n) -> float:\n    updated_start_time = max(updated_start_times) if updated_start_times else ideal_start_time\n    if updated_start_time != ideal_start_time:\n        # start time changed: remove extra candles\n        _get_logger().warning(\n            f\"Adapting backtesting start time according to data availability. \"\n            f\"Updated start time: {timestamp_util.convert_timestamp_to_datetime(updated_start_time)}. \"\n            f\"Initial start time: {timestamp_util.convert_timestamp_to_datetime(ideal_start_time)}\"\n        )\n        adapt_exchange_data_for_updated_start_time(exchange_data, updated_start_time)\n    return updated_start_time\n\n\ndef ensure_ohlcv_validity(\n    ohlcvs: list, exchange: str, symbol: str, time_frame: common_enums.TimeFrames,\n    start_time: float, last_open_time: float, required_from_the_start: bool, required_till_the_end: bool,\n    first_traded_symbols_time: float, allow_any_backtesting_start_and_end_time: bool\n) -> typing.Optional[float]:\n    if not ohlcvs:\n        raise errors.InvalidBacktestingDataError(f\"No {symbol} {time_frame.value} {exchange} OHLCV data\")\n    # ensure history is going approximately to start_time\n    first_candle_time = ohlcvs[0][common_enums.PriceIndexes.IND_PRICE_TIME.value]\n    last_candle_time = ohlcvs[-1][common_enums.PriceIndexes.IND_PRICE_TIME.value]\n    return ensure_compatible_candle_time(\n        exchange, symbol, time_frame, start_time, last_open_time, first_candle_time, last_candle_time,\n        False, required_from_the_start, required_till_the_end, first_traded_symbols_time,\n        allow_any_backtesting_start_and_end_time\n    )\n\n\ndef adapt_exchange_data_for_updated_start_time(\n    exchange_data: exchange_data_import.ExchangeData, first_candle_time: float\n):\n    _get_logger().info(f\"Filtering out backtesting candles to start at {first_candle_time}\")\n    for market in exchange_data.markets:\n        market.time = [\n            candle_time\n            for candle_time in market.time\n            if candle_time >= first_candle_time\n        ]\n        market.close = market.close[-len(market.time):]\n        market.open = market.open[-len(market.time):]\n        market.high = market.high[-len(market.time):]\n        market.low = market.low[-len(market.time):]\n        market.volume = market.volume[-len(market.time):]\n\n\ndef ensure_compatible_candle_time(\n    exchange: str, symbol: str, time_frame: common_enums.TimeFrames,\n    first_open_time: float, last_open_time: float, first_candle_time: float, last_candle_time: float,\n    allow_candles_beyond_range: bool, required_from_the_start: bool, required_till_the_end: bool,\n    first_traded_symbols_time: float, allow_any_backtesting_start_and_end_time: bool\n) -> typing.Optional[float]:\n    adapted_start_time = None\n    # ensure history is going approximately to first_open_time\n    if not allow_candles_beyond_range:\n        # first_candle_time starting before the first_open_time (more candles than required)\n        if first_candle_time < first_open_time - constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW:\n            raise errors.InvalidBacktestingDataError(\n                f\"{symbol} {time_frame.value} {exchange} OHLCV data starts too early \"\n                f\"({first_candle_time} vs {first_open_time})\"\n            )\n    time_frame_seconds = common_enums.TimeFramesMinutes[time_frame] * common_constants.MINUTE_TO_SECONDS\n    if first_candle_time > first_open_time + time_frame_seconds:\n        if required_from_the_start:\n            max_allowed_delayed_start = first_traded_symbols_time + constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW\n            # missing initial candles, align start time to the first candle time when possible\n            if allow_any_backtesting_start_and_end_time or first_candle_time < max_allowed_delayed_start:\n                adapted_start_time = first_candle_time \n                _get_logger().info(\n                    f\"{symbol} {time_frame.value} {exchange} OHLCV data starts too late \"\n                    f\"({first_candle_time} vs {first_open_time}): this is acceptable, start time is adapted to \"\n                    f\"{first_candle_time} (delta: {datetime.timedelta(seconds=first_candle_time - first_open_time)})\"\n                )\n            else:\n                raise errors.InvalidBacktestingDataError(\n                    f\"{symbol} {time_frame.value} {exchange} OHLCV data starts too late \"\n                    f\"({first_candle_time} vs {first_open_time})\"\n                )\n        else:\n            _get_logger().info(\n                f\"{symbol} {time_frame.value} {exchange} OHLCV data starts too late \"\n                f\"({first_candle_time} vs {first_open_time}): this is acceptable, this symbol is not required from \"\n                f\"the start\"\n            )\n    # ensure history is going approximately until last_open_time\n    if not allow_candles_beyond_range:\n        # last_open_time ending after the last_candle_time (more candles than required)\n        if last_open_time < last_candle_time:\n            raise errors.InvalidBacktestingDataError(\n                f\"{symbol} {time_frame.value} {exchange} OHLCV data ends too late ({last_open_time} vs {last_candle_time})\"\n            )\n\n    if last_open_time - constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW > last_candle_time:\n        if required_till_the_end:\n            raise errors.InvalidBacktestingDataError(\n                f\"{symbol} {time_frame.value} {exchange} OHLCV data ends too early ({last_candle_time} vs {last_open_time})\"\n            )\n        else:\n            _get_logger().info(\n                f\"{symbol} {time_frame.value} {exchange} OHLCV data ends too early \"\n                f\"({last_candle_time} vs {last_open_time}): this is acceptable, this symbol is not required till \"\n                f\"the end of the run\"\n            )\n    if adapted_start_time is not None and not allow_any_backtesting_start_and_end_time:\n        # ensure adapted_start_time is not reducing too much the global backtesting duration\n        ideal_duration = last_open_time - first_open_time\n        adapted_duration = last_candle_time - adapted_start_time\n        if adapted_duration < ideal_duration * constants.BACKTESTING_MIN_DURATION_RATIO:\n            raise errors.InvalidBacktestingDataError(\n                f\"{symbol} {time_frame.value} {exchange} OHLCV adapted backtesting start time starts too late resulting \"\n                f\"in a {round(adapted_duration/common_constants.DAYS_TO_SECONDS, 1)} days backtesting duration \"\n                f\"vs {round(ideal_duration/common_constants.DAYS_TO_SECONDS, 1)} ideal days. Min allowed is \"\n                f\"{round(ideal_duration * constants.BACKTESTING_MIN_DURATION_RATIO / common_constants.DAYS_TO_SECONDS, 1)} days.\"\n            )\n    return adapted_start_time\n\n\ndef _get_logger():\n    return octobot_commons.logging.get_logger(\"ScriptingBacktestingDataCollector\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/backtesting_data_selector.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_backtesting.api as backtesting_api\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_public_data as exchange_public_data\n\n\ndef backtesting_start_time(ctx):\n    return backtesting_api.get_backtesting_starting_time(ctx.exchange_manager.exchange.backtesting)\n\n\ndef backtesting_first_full_candle_time(ctx):\n    return _align_time_to_time_frame(backtesting_start_time(ctx), ctx.time_frame, False)\n\n\nasync def backtesting_is_first_full_candle(ctx):\n    current_t = await exchange_public_data.current_candle_time(ctx)\n    first_c = _align_time_to_time_frame(backtesting_start_time(ctx), ctx.time_frame, False)\n    return current_t == first_c\n\n\ndef backtesting_end_time(ctx):\n    return backtesting_api.get_backtesting_ending_time(ctx.exchange_manager.exchange.backtesting)\n\n\ndef backtesting_last_full_candle_time(ctx):\n    return _align_time_to_time_frame(backtesting_end_time(ctx), ctx.time_frame, True)\n\n\ndef _align_time_to_time_frame(reference_time, time_frame, align_backwards):\n    time_frame_sec = commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(time_frame)] \\\n        * commons_constants.MINUTE_TO_SECONDS\n    time_delta = reference_time % time_frame_sec\n    if align_backwards:\n        # the last full candle time is the backtesting end time moved back to the start of the candle\n        potential_candle_time = reference_time - time_frame_sec\n    else:\n        # the first full candle time the backtesting start time moved forward to the start of the 1st candle\n        potential_candle_time = reference_time - time_frame_sec\n        time_delta = time_frame_sec - time_delta if time_delta > 0 else 0\n    # align back to the UTC time\n    return potential_candle_time - time_delta if align_backwards else potential_candle_time + time_delta\n\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/backtesting_intialization.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport logging\nimport contextlib\nimport typing\n\nimport octobot_commons.profiles as commons_profiles\nimport octobot_commons.configuration as commons_configuration\nimport octobot_commons.logging as commons_logging\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.list_util as list_util\nimport octobot_commons.enums as common_enums\n\nimport octobot_backtesting.backtest_data\nimport octobot_backtesting.api\n\nimport octobot_tentacles_manager.configuration\n\nimport octobot.backtesting.independent_backtesting\nimport octobot.backtesting.minimal_data_importer as minimal_data_importer\n\nimport octobot_trading.util.test_tools.exchange_data as exchange_data_import\nimport octobot_trading.api\n\nimport tentacles.Meta.Keywords.scripting_library as scripting_library\n\n\n@contextlib.asynccontextmanager\nasync def init_and_run_backtesting(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n) -> typing.AsyncGenerator[octobot.backtesting.independent_backtesting.IndependentBacktesting, None]:\n    \"\"\"\n    Initialize and run backtesting.\n    Usage:\n    async with init_and_run_backtesting(exchange_data, profile_data) as independent_backtesting:\n        # use independent_backtesting to get backtesting results before it gets stopped\n    \"\"\"\n    async with run_backtesting(\n        exchange_data, \n        profile_data, \n        scripting_library.create_backtesting_config(profile_data, exchange_data),\n        scripting_library.get_full_tentacles_setup_config(profile_data),\n    ) as independent_backtesting:\n        yield independent_backtesting\n\n\n@contextlib.asynccontextmanager\nasync def run_backtesting(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n    backtesting_config: commons_configuration.Configuration,\n    tentacles_config: octobot_tentacles_manager.configuration.TentaclesSetupConfiguration,\n    enable_logs: bool = False,\n) -> typing.AsyncGenerator[octobot.backtesting.independent_backtesting.IndependentBacktesting, None]:\n    with octobot_tentacles_manager.configuration.local_get_config_proxy(scripting_library.empty_config_proxy):\n        backtest_data = await _init_backtest_data(\n            exchange_data, backtesting_config, tentacles_config\n        )\n        independent_backtesting = None\n        try:\n            with commons_logging.temporary_log_level(logging.INFO):\n                independent_backtesting = _init_independent_backtesting(\n                    exchange_data, profile_data, backtest_data, enable_logs=enable_logs\n                )\n                await independent_backtesting.initialize_and_run(log_errors=True)\n                await independent_backtesting.join_backtesting_updater(None)\n            # independent_backtesting.log_report()  # uncomment to debug\n            yield independent_backtesting\n        finally:\n            if independent_backtesting is not None:\n                with commons_logging.temporary_log_level(logging.INFO):\n                    await independent_backtesting.clear_fetched_data()\n                    await independent_backtesting.stop(memory_check=False, should_raise=False)\n\n\ndef _init_independent_backtesting(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData,\n    backtest_data: octobot_backtesting.backtest_data.BacktestData,\n    enable_logs: bool = False,\n) -> \"octobot.backtesting.independent_backtesting.IndependentBacktesting\":\n    independent_backtesting = octobot.backtesting.independent_backtesting.IndependentBacktesting(\n        backtest_data.config,\n        backtest_data.tentacles_config,\n        backtest_data.data_files,\n        run_on_common_part_only=True,\n        start_timestamp=None,\n        end_timestamp=None,\n        enable_logs=enable_logs,\n        stop_when_finished=False,\n        run_on_all_available_time_frames=False,\n        enforce_total_databases_max_size_after_run=False,\n        enable_storage=False,\n        backtesting_data=backtest_data,\n        config_by_tentacle={\n            tentacle.name: tentacle.config\n            for tentacle in profile_data.tentacles\n        },\n        services_config={},\n    )\n    independent_backtesting.symbols_to_create_exchange_classes.update({\n        exchange: [\n            commons_symbols.parse_symbol(s)\n            for s in list_util.deduplicate([\n                market_details.symbol\n                for market_details in exchange_data.markets\n                if market_details.has_full_candles()\n            ])\n        ]\n        for exchange in [exchange_data.exchange_details.name]   # TODO handle multi exchanges\n    })\n    return independent_backtesting\n\n\nasync def _init_backtest_data(\n    exchange_data: exchange_data_import.ExchangeData,\n    backtesting_config: commons_configuration.Configuration,\n    tentacles_config: octobot_tentacles_manager.configuration.TentaclesSetupConfiguration,\n) -> octobot_backtesting.backtest_data.BacktestData:\n    backtest_data = await octobot_backtesting.api.create_and_init_backtest_data(\n        [], backtesting_config.config, tentacles_config, True\n    )\n    backtest_data.use_cached_markets = True\n    await _init_importers(exchange_data, backtest_data)\n    importer = next(iter(backtest_data.importers_by_data_file.values()))\n    start_time, end_time = await importer.get_data_timestamp_interval()\n    await _init_preloaded_candle_managers(exchange_data, backtest_data, start_time, end_time)\n    return backtest_data\n\n\nasync def _init_importers(\n    exchange_data: exchange_data_import.ExchangeData,\n    backtest_data: octobot_backtesting.backtest_data.BacktestData,\n):\n    backtest_data.data_files = [f\"simulated_{exchange_data.exchange_details.name}_file.data\"]\n    backtest_data.default_importer = minimal_data_importer.MinimalDataImporter # type: ignore\n    await backtest_data.initialize()\n    for importer in backtest_data.importers_by_data_file.values():\n        importer.update_from_exchange_data(exchange_data) # type: ignore\n\n\nasync def _init_preloaded_candle_managers(\n    exchange_data: exchange_data_import.ExchangeData,\n    backtest_data: octobot_backtesting.backtest_data.BacktestData,\n    start_time,\n    end_time\n):\n    for exchange_details in [exchange_data.exchange_details]:\n        for market_details in exchange_data.markets:\n            if not market_details.has_full_candles():\n                continue\n            key = backtest_data._get_key(\n                exchange_details.name, market_details.symbol, common_enums.TimeFrames(market_details.time_frame),\n                start_time, end_time\n            )\n            backtest_data.preloaded_candle_managers[key] = await octobot_trading.api.create_preloaded_candles_manager(\n                market_details.get_formatted_candles()\n            )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/backtesting_settings.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_backtesting.api as backtesting_api\n\n\ndef set_backtesting_iteration_timeout(ctx, iteration_timeout_in_seconds: int):\n    if ctx.exchange_manager.is_backtesting:\n        backtesting_api.set_iteration_timeout(\n            ctx.exchange_manager.exchange.backtesting,\n            iteration_timeout_in_seconds\n        )\n\n\ndef register_backtesting_timestamp_whitelist(ctx, timestamps, check_callback=None, append_to_whitelist=True):\n    if check_callback is None:\n        def _open_order_and_position_check():\n            # by default, avoid skipping timestamps when there are open orders or active positions\n            if ctx.exchange_manager.exchange_personal_data.orders_manager.get_open_orders():\n                return True\n            for position in ctx.exchange_manager.exchange_personal_data.positions_manager.positions.values():\n                if not position.is_idle():\n                    return True\n            return False\n\n        check_callback = _open_order_and_position_check\n    if ctx.exchange_manager.is_backtesting and \\\n            backtesting_api.get_backtesting_timestamp_whitelist(ctx.exchange_manager.exchange.backtesting) \\\n            != sorted(set(timestamps)):\n        return backtesting_api.register_backtesting_timestamp_whitelist(\n            ctx.exchange_manager.exchange.backtesting,\n            timestamps,\n            check_callback,\n            append_to_whitelist=append_to_whitelist\n        )\n\n\ndef is_registered_backtesting_timestamp_whitelist(ctx):\n    return ctx.exchange_manager.is_backtesting and \\\n            backtesting_api.get_backtesting_timestamp_whitelist(ctx.exchange_manager.exchange.backtesting) is not None\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/default_backtesting_run_analysis_script.py",
    "content": "import datetime as datetime\nimport json as json\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.pretty_printer as pretty_printer\nimport octobot_services.constants as services_constants\nimport tentacles.Meta.Keywords.scripting_library.backtesting.run_data_analysis as run_data_analysis\nimport octobot_trading.modes.script_keywords as script_keywords\n\n\nasync def default_backtesting_analysis_script(ctx: script_keywords.Context):\n    async with ctx.backtesting_results() as (run_data, run_display):\n        historical_values = await run_data_analysis.load_historical_values(run_data, None)\n        if ctx.backtesting_analysis_settings[\"plot_pnl_on_main_chart\"]:\n            with run_display.part(\"main-chart\") as part:\n                try:\n                    await run_data_analysis.plot_historical_portfolio_value(\n                        run_data, part,\n                        historical_values=historical_values,\n                    )\n                    await run_data_analysis.plot_historical_pnl_value(\n                        run_data, part, x_as_trade_count=False,\n                        own_yaxis=True,\n                        include_unitary=ctx.backtesting_analysis_settings[\"plot_trade_gains_on_main_chart\"],\n                        historical_values=historical_values,\n                    )\n                except Exception as err:\n                    ctx.logger.exception(err, True, f\"Error when computing main chant graphs {err}\")\n        with run_display.part(\"backtesting-run-overview\") as part:\n            try:\n                if ctx.backtesting_analysis_settings.get(\"plot_hist_portfolio_on_backtesting_chart\", True):\n                    await run_data_analysis.plot_historical_portfolio_value(\n                        run_data, part,\n                        historical_values=historical_values,\n                    )\n                if ctx.backtesting_analysis_settings[\"plot_pnl_on_backtesting_chart\"]:\n                    await run_data_analysis.plot_historical_pnl_value(\n                        run_data, part, x_as_trade_count=False,\n                        own_yaxis=True,\n                        include_unitary=ctx.backtesting_analysis_settings[\"plot_trade_gains_on_backtesting_chart\"],\n                        historical_values=historical_values,\n                    )\n                if ctx.backtesting_analysis_settings[\"plot_best_case_growth_on_backtesting_chart\"]:\n                    await run_data_analysis.plot_best_case_growth(\n                        run_data, part, x_as_trade_count=True, own_yaxis=False,\n                        historical_values=historical_values,\n                    )\n                if ctx.backtesting_analysis_settings[\"plot_funding_fees_on_backtesting_chart\"]:\n                    await run_data_analysis.plot_historical_funding_fees(\n                        run_data, part, own_yaxis=True,\n                    )\n                if ctx.backtesting_analysis_settings[\"plot_wins_and_losses_count_on_backtesting_chart\"]:\n                    await run_data_analysis.plot_historical_wins_and_losses(\n                        run_data, part, own_yaxis=True, x_as_trade_count=False,\n                        historical_values=historical_values,\n                    )\n                if ctx.backtesting_analysis_settings[\"plot_win_rate_on_backtesting_chart\"]:\n                    await run_data_analysis.plot_historical_win_rates(\n                        run_data, part, own_yaxis=True, x_as_trade_count=False,\n                        historical_values=historical_values,\n                    )\n                # await plot_withdrawals(run_data, part)\n            except Exception as err:\n                ctx.logger.exception(err, True, f\"Error when computing run overview graphs {err}\")\n        if ctx.backtesting_analysis_settings[\"display_backtest_details\"]:\n            with run_display.part(\"backtesting-details\", \"value\") as part:\n                try:\n                    backtesting_report = await get_backtesting_report_template(\n                        run_data, ctx.backtesting_analysis_settings, historical_values\n                    )\n                    await run_data_analysis.display_html(part, backtesting_report)\n                except Exception as err:\n                    ctx.logger.exception(err, True, f\"Error when computing details part {err}\")\n        if ctx.backtesting_analysis_settings[\"display_trades_and_positions\"]:\n            with run_display.part(\"list-of-trades-part\", \"table\") as part:\n                try:\n                    await run_data_analysis.plot_trades(run_data, part, historical_values=historical_values)\n                    await run_data_analysis.plot_orders(run_data, part, historical_values=historical_values)\n                    await run_data_analysis.plot_positions(run_data, part)\n                    # await plot_table(run_data, part, \"SMA 1\")  # plot any cache key as a table\n                except Exception as err:\n                    ctx.logger.exception(err, True, f\"Error when computing trades part {err}\")\n    return run_display\n\n\nasync def get_backtesting_report_template(run_data, backtesting_analysis_settings, historical_values):\n    price_data, _, _, _, _, metadata = historical_values\n    optimizer_id_display = get_column_display(commons_enums.BacktestingMetadata.OPTIMIZER_ID.value,\n                                              commons_enums.BacktestingMetadata.OPTIMIZER_ID.value) \\\n        if commons_enums.BacktestingMetadata.OPTIMIZER_ID.value in metadata.keys() else \"\"\n    paid_fees_display = get_column_display(services_constants.PAID_FEES_STR,\n                                           metadata[\"paid_fees\"]) if \"paid_fees\" in metadata.keys() else \"\"\n    performance_summary = \"\"\n    reference_market = metadata[commons_enums.DBRows.REFERENCE_MARKET.value]\n    if backtesting_analysis_settings.get(\"display_backtest_details_performances\", True):\n        start_portfolio_value, end_portfolio_value = await run_data_analysis.get_portfolio_values(run_data)\n        gains = f\"{pretty_printer.get_min_string_from_number(metadata[commons_enums.BacktestingMetadata.GAINS.value])} \" \\\n                f\"({pretty_printer.get_min_string_from_number(metadata[commons_enums.BacktestingMetadata.PERCENT_GAINS.value])}%)\"\n        performance_summary \\\n            += get_section_display(\"Performance\",\n                                   get_column_display(commons_enums.BacktestingMetadata.START_PORTFOLIO.value,\n                                                      get_portfolio_display(\n                                                          metadata[commons_enums.BacktestingMetadata.START_PORTFOLIO.value]\n                                                      )) +\n                                   get_column_display(commons_enums.BacktestingMetadata.END_PORTFOLIO.value,\n                                                      get_portfolio_display(\n                                                          metadata[\n                                                              commons_enums.BacktestingMetadata.END_PORTFOLIO.value])) +\n                                   get_column_display(f\"{commons_enums.BacktestingMetadata.START_PORTFOLIO.value} \"\n                                                      f\"{reference_market} value\",\n                                                      start_portfolio_value) +\n                                   get_column_display(f\"{commons_enums.BacktestingMetadata.END_PORTFOLIO.value} \"\n                                                      f\"{reference_market} value\",\n                                                      end_portfolio_value) +\n                                   get_column_display(f\"{reference_market} gains\", gains) +\n                                   get_column_display(\n                                       commons_enums.BacktestingMetadata.MARKETS_PROFITABILITY.value,\n                                       metadata.get(commons_enums.BacktestingMetadata.MARKETS_PROFITABILITY.value, {})\n                                   ) +\n                                   get_column_display(\n                                       commons_enums.BacktestingMetadata.TRADES.value + \" (entries and exits)\",\n                                       metadata[commons_enums.BacktestingMetadata.TRADES.value])\n                                   # todo fix those values\n                                   # + get_column_display(commons_enums.BacktestingMetadata.ENTRIES.value,\n                                   #                    metadata[commons_enums.BacktestingMetadata.ENTRIES.value]) +\n                                   # get_column_display(commons_enums.BacktestingMetadata.WINS.value,\n                                   #                    metadata[commons_enums.BacktestingMetadata.WINS.value]) +\n                                   # get_column_display(commons_enums.BacktestingMetadata.LOSES.value,\n                                   #                    metadata[commons_enums.BacktestingMetadata.LOSES.value]) +\n                                   # get_column_display(commons_enums.BacktestingMetadata.WIN_RATE.value,\n                                   #                    metadata[commons_enums.BacktestingMetadata.WIN_RATE.value]) +\n                                   # get_column_display(commons_enums.BacktestingMetadata.DRAW_DOWN.value,\n                                   #                    metadata[commons_enums.BacktestingMetadata.DRAW_DOWN.value]) +\n                                   # get_column_display(\n                                   #     commons_enums.BacktestingMetadata.COEFFICIENT_OF_DETERMINATION_MAX_BALANCE.value,\n                                   #     metadata[commons_enums.BacktestingMetadata\n                                   #         .COEFFICIENT_OF_DETERMINATION_MAX_BALANCE.value]) +\n                                   # paid_fees_display\n                                   )\n    if backtesting_analysis_settings.get(\"display_backtest_details_general\", True):\n        performance_summary \\\n            += get_section_display(\"General\",\n                                  get_column_display(commons_enums.BacktestingMetadata.NAME.value,\n                                                     metadata[commons_enums.BacktestingMetadata.NAME.value])\n                                  + get_column_display(commons_enums.BacktestingMetadata.OPTIMIZATION_CAMPAIGN.value,\n                                                       metadata[commons_enums.BacktestingMetadata.\n                                                       OPTIMIZATION_CAMPAIGN.value])\n                                  + optimizer_id_display\n                                  + get_column_display(commons_enums.BacktestingMetadata.ID.value,\n                                                       metadata[commons_enums.BacktestingMetadata.ID.value])\n                                  + get_column_display(commons_enums.DBRows.EXCHANGES.value,\n                                                       metadata[commons_enums.DBRows.EXCHANGES.value])\n                                  + get_column_display(commons_enums.BacktestingMetadata.BACKTESTING_FILES.value,\n                                                       metadata[commons_enums.BacktestingMetadata.BACKTESTING_FILES.value]))\n    if backtesting_analysis_settings.get(\"display_backtest_details_details\", True):\n        performance_summary \\\n            += get_section_display(\"Details\",\n                                   get_column_display(commons_enums.BacktestingMetadata.TIME_FRAMES.value,\n                                                      get_badges_from_list(\n                                                          metadata[commons_enums.BacktestingMetadata.TIME_FRAMES.value])) +\n                                   get_column_display(commons_enums.BacktestingMetadata.START_TIME.value,\n                                                      datetime.datetime.fromtimestamp(\n                                                          metadata[commons_enums.DBRows.START_TIME.value])) +\n                                   get_column_display(commons_enums.BacktestingMetadata.END_TIME.value,\n                                                      datetime.datetime.fromtimestamp(\n                                                          metadata[commons_enums.DBRows.END_TIME.value])) +\n                                   get_column_display(commons_enums.BacktestingMetadata.SYMBOLS.value,\n                                                      get_badges_from_list(\n                                                          metadata[commons_enums.BacktestingMetadata.SYMBOLS.value])) +\n                                   get_column_display(f\"{commons_enums.BacktestingMetadata.DURATION.value} (s)\",\n                                                      metadata[commons_enums.BacktestingMetadata.DURATION.value]) +\n                                   get_column_display(commons_enums.BacktestingMetadata.LEVERAGE.value,\n                                                      metadata[commons_enums.BacktestingMetadata.LEVERAGE.value]) +\n                                   get_column_display(\"Backtesting time\",\n                                                      datetime.datetime.fromtimestamp(\n                                                          metadata[commons_enums.BacktestingMetadata.TIMESTAMP.value]))\n                                   )\n\n    if backtesting_analysis_settings.get(\"display_backtest_details_strategy_settings\", True):\n        performance_summary \\\n            += get_section_display(\"Strategy Settings\",\n                                   get_user_inputs_display(metadata)\n                                   )\n\n    return performance_summary\n\n\ndef get_section_display(title, content):\n    return f''' \n        <div data-role=\"values\" class=\"backtesting-run-container-values container-fluid row mb-5\">\n            <div class=\"col-12\">\n                <h4 class=\"text-center\">{title}</h4>\n            </div>\n            {content}\n        </div>\n    '''\n\n\ndef get_column_display(title, value):\n    return f'''\n        <div class=\"col-6 col-md-3  text-center\">\n            <div class=\"backtesting-run-container-values-label text-capitalize\">\n                {title}\n            </div>\n            <div class=\"backtesting-run-container-values-value\">\n                {', '.join(value) if (isinstance(value, list) and value and isinstance(value[0], (int, float, str)))\n                    else pretty_printer.get_min_string_from_number(value) if isinstance(value, float) \n                    else ', '.join(f\"{key}: {val}\" for key, val in value.items()) if isinstance(value, dict) \n                    else value}\n            </div>\n        </div>\n    '''\n\n\ndef get_badges_from_list(_list):\n    _html = \"\"\n    for _item in _list:\n        _html += f'<span class=\"badge badge-light mx-1\">{_item}</span>'\n    return _html\n\n\ndef get_portfolio_display(_dict):\n    _html = \"\"\n    _dict_str = _dict.replace(\"\\'\", '\"')\n    _dict_str = json.loads(_dict_str)\n    for _key in _dict_str:\n        _html += f'<span class=\"mx-1\">{_key}: {pretty_printer.get_min_string_from_number(_dict_str[_key][\"total\"])}</span>'\n    return _html\n\n\ndef get_user_inputs_display(metadata):\n    content = \"\"\n    for _evaluator in metadata['user inputs']:\n        _section_content = \"\"\n        for input_name in metadata['user inputs'][_evaluator]:\n            _section_content += get_column_display(input_name, metadata['user inputs'][_evaluator][input_name])\n\n        content += get_section_display(_evaluator, _section_content)\n    return content\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/metadata.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.databases as databases\nimport octobot_commons.errors as commons_errors\nimport octobot_commons.enums as commons_enums\nimport tentacles.Meta.Keywords.scripting_library.data as data\n\n\ndef set_script_name(ctx, name):\n    ctx.tentacle.script_name = name\n\n\nasync def _read_backtesting_metadata(optimizer_run_dbs_identifier, metadata_list, optimizer_id):\n    async with data.MetadataReader.database(optimizer_run_dbs_identifier.get_backtesting_metadata_identifier()) \\\n            as reader:\n        try:\n            metadata = await reader.read()\n            for metadata_element in metadata:\n                metadata_element[commons_enums.BacktestingMetadata.OPTIMIZER_ID.value] = optimizer_id\n            metadata_list += metadata\n        except commons_errors.DatabaseNotFoundError:\n            pass\n\n\nasync def read_metadata(runs_to_load_settings, trading_mode, include_optimizer_runs=False):\n    metadata = []\n    optimizer_run_dbs_identifiers = []\n    run_dbs_identifier = databases.RunDatabasesIdentifier(trading_mode)\n    try:\n        campaigns_to_load = runs_to_load_settings[\"campaigns\"]\n    except KeyError:\n        campaigns_to_load = runs_to_load_settings[\"campaigns\"] = {}\n    available_campaigns = await run_dbs_identifier.get_optimization_campaign_names()\n    campaigns = {}\n    for optimization_campaign_name in available_campaigns:\n        if optimization_campaign_name in campaigns_to_load:\n            if campaigns_to_load[optimization_campaign_name]:\n                campaigns[optimization_campaign_name] = True\n            else:\n                campaigns[optimization_campaign_name] = False\n                continue\n        else:\n            campaigns[optimization_campaign_name] = True\n\n        backtesting_run_dbs_identifier = databases.RunDatabasesIdentifier(trading_mode, optimization_campaign_name,\n                                                                          backtesting_id=\"1\")\n        if include_optimizer_runs:\n            optimizer_ids = await backtesting_run_dbs_identifier.get_optimizer_run_ids()\n            if optimizer_ids:\n                optimizer_run_dbs_identifiers = [\n                    databases.RunDatabasesIdentifier(trading_mode, optimization_campaign_name,\n                                                     optimizer_id=optimizer_id)\n                    for optimizer_id in optimizer_ids]\n        try:\n            await _read_backtesting_metadata(backtesting_run_dbs_identifier, metadata, 0)\n        except commons_errors.DatabaseNotFoundError:\n            pass\n        for optimizer_run_dbs_identifier in optimizer_run_dbs_identifiers:\n            await _read_backtesting_metadata(optimizer_run_dbs_identifier, metadata,\n                                             optimizer_run_dbs_identifier.optimizer_id)\n    return campaigns, metadata\n\n\nasync def _read_bot_recording_metadata(run_dbs_identifier, metadata_list):\n    async with data.MetadataReader.database(run_dbs_identifier.get_bot_live_metadata_identifier()) \\\n            as reader:\n        try:\n            metadata = await reader.read()\n            metadata_list += metadata\n        except commons_errors.DatabaseNotFoundError:\n            pass\n\n\nasync def read_bot_recording_runs_metadata(trading_mode):\n    metadata = []\n    run_dbs_identifier = databases.RunDatabasesIdentifier(trading_mode)\n    try:\n        await _read_bot_recording_metadata(run_dbs_identifier, metadata)\n    except commons_errors.DatabaseNotFoundError:\n        pass\n    return metadata\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/backtesting/run_data_analysis.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport json\nimport sortedcontainers\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchange_data as trading_exchange_data\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.personal_data.portfolios.portfolio_util as portfolio_util\nimport octobot_trading.api as trading_api\nimport octobot_backtesting.api as backtesting_api\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.constants\nimport octobot_commons.databases as databases\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.errors as commons_errors\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.logging\n\n\ndef get_logger():\n    return octobot_commons.logging.get_logger(\"BacktestingRunData\")\n\n\nasync def get_candles(candles_sources, exchange, symbol, time_frame, metadata):\n    return await backtesting_api.get_all_ohlcvs(candles_sources[0][commons_enums.DBRows.VALUE.value],\n                                                exchange,\n                                                symbol,\n                                                commons_enums.TimeFrames(time_frame),\n                                                inferior_timestamp=metadata[commons_enums.DBRows.START_TIME.value],\n                                                superior_timestamp=metadata[commons_enums.DBRows.END_TIME.value])\n\n\nasync def get_trades(meta_database, metadata, symbol):\n    account_type = trading_api.get_account_type_from_run_metadata(metadata)\n    return await meta_database.get_trades_db(account_type).select(\n        commons_enums.DBTables.TRADES.value,\n        (await meta_database.get_trades_db(account_type).search()).symbol == symbol\n    )\n\n\nasync def get_metadata(meta_database):\n    return (await meta_database.get_run_db().all(commons_enums.DBTables.METADATA.value))[0]\n\n\nasync def get_transactions(meta_database, transaction_type=None, transaction_types=None):\n    account_type = trading_api.get_account_type_from_run_metadata(await get_metadata(meta_database))\n    if transaction_type is not None:\n        query = (await meta_database.get_transactions_db(account_type).search()).type == transaction_type\n    elif transaction_types is not None:\n        query = (await meta_database.get_transactions_db(account_type).search()).type.one_of(transaction_types)\n    else:\n        return await meta_database.get_transactions_db(account_type).all(commons_enums.DBTables.TRANSACTIONS.value)\n    return await meta_database.get_transactions_db(account_type).select(commons_enums.DBTables.TRANSACTIONS.value,\n                                                                        query)\n\n\nasync def get_starting_portfolio(meta_database) -> dict:\n    portfolio = (await meta_database.get_run_db().all(commons_enums.DBTables.METADATA.value))[0][\n        commons_enums.BacktestingMetadata.START_PORTFOLIO.value]\n    return json.loads(portfolio.replace(\"'\", '\"'))\n\n\nasync def load_historical_values(meta_database, exchange, with_candles=True,\n                                 with_trades=True, with_portfolio=True, time_frame=None):\n    price_data = {}\n    trades_data = {}\n    moving_portfolio_data = {}\n    trading_type = \"spot\"\n    metadata = {}\n    run_global_metadata = {}\n    try:\n        starting_portfolio = await get_starting_portfolio(meta_database)\n        metadata = await get_metadata(meta_database)\n        run_global_metadata = await meta_database.get_backtesting_metadata_from_run()\n\n        exchange = exchange or meta_database.run_dbs_identifier.context.exchange_name \\\n            or metadata[commons_enums.DBRows.EXCHANGES.value][0]  # TODO handle multi exchanges\n        ref_market = metadata[commons_enums.DBRows.REFERENCE_MARKET.value]\n        trading_type = metadata[commons_enums.DBRows.TRADING_TYPE.value]\n        contracts = metadata[commons_enums.DBRows.FUTURE_CONTRACTS.value][exchange] if trading_type == \"future\" else {}\n        # init data\n        for pair in run_global_metadata[commons_enums.DBRows.SYMBOLS.value]:\n            symbol = symbol_util.parse_symbol(pair).base\n            is_inverse_contract = trading_type == \"future\" and trading_api.is_inverse_future_contract(\n                trading_enums.FutureContractType(contracts[pair][\"contract_type\"])\n            )\n            if symbol != ref_market or is_inverse_contract:\n                candles_sources = await meta_database.get_symbol_db(exchange, pair).all(\n                    commons_enums.DBTables.CANDLES_SOURCE.value\n                )\n                if time_frame is None:\n                    time_frames = [source[commons_enums.DBRows.TIME_FRAME.value] for source in candles_sources]\n                    time_frame = time_frame_manager.find_min_time_frame(time_frames) if time_frames else time_frame\n                if with_candles and pair not in price_data:\n                    # convert candles timestamp in millis\n                    raw_candles = await get_candles(candles_sources, exchange, pair, time_frame, metadata)\n                    for candle in raw_candles:\n                        candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] = \\\n                            candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value] * 1000\n                    price_data[pair] = raw_candles\n                if with_trades and pair not in trades_data:\n                    trades_data[pair] = await get_trades(meta_database, metadata, pair)\n            if with_portfolio:\n                try:\n                    moving_portfolio_data[symbol] = starting_portfolio[symbol][\n                        octobot_commons.constants.PORTFOLIO_TOTAL]\n                except KeyError:\n                    moving_portfolio_data[symbol] = 0\n                try:\n                    moving_portfolio_data[ref_market] = starting_portfolio[ref_market][\n                        octobot_commons.constants.PORTFOLIO_TOTAL]\n                except KeyError:\n                    moving_portfolio_data[ref_market] = 0\n    except IndexError:\n        pass\n    return price_data, trades_data, moving_portfolio_data, trading_type, metadata, run_global_metadata\n\n\nasync def backtesting_data(meta_database, data_label):\n    metadata_from_run = await meta_database.get_backtesting_metadata_from_run()\n    for key, value in metadata_from_run.items():\n        if key == data_label:\n            return value\n    account_type = trading_api.get_account_type_from_run_metadata(metadata_from_run)\n    for reader in meta_database.all_basic_run_db(account_type):\n        for table in await reader.tables():\n            if table == data_label:\n                return await reader.all(table)\n            for row in await reader.all(table):\n                for key, value in row.items():\n                    if key == data_label:\n                        return value\n    return None\n\n\nasync def _get_grouped_funding_fees(meta_database, group_key):\n    funding_fees_history = await get_transactions(meta_database,\n                                                  transaction_type=trading_enums.TransactionType.FUNDING_FEE.value)\n    funding_fees_history = sorted(funding_fees_history, key=lambda f: f[commons_enums.PlotAttributes.X.value])\n    funding_fees_history_by_key = {}\n    for funding_fee in funding_fees_history:\n        try:\n            funding_fees_history_by_key[funding_fee[group_key]].append(funding_fee)\n        except KeyError:\n            funding_fees_history_by_key[funding_fee[group_key]] = [funding_fee]\n    return funding_fees_history_by_key\n\n\nasync def plot_historical_funding_fees(meta_database, plotted_element, own_yaxis=True):\n    funding_fees_history_by_currency = await _get_grouped_funding_fees(\n        meta_database,\n        trading_enums.FeePropertyColumns.CURRENCY.value\n    )\n    for currency, fees in funding_fees_history_by_currency.items():\n        cumulative_fees = []\n        previous_fee = 0\n        for fee in fees:\n            cumulated_fee = fee[\"quantity\"] + previous_fee\n            cumulative_fees.append(cumulated_fee)\n            previous_fee = cumulated_fee\n        plotted_element.plot(\n            mode=\"scatter\",\n            x=[fee[commons_enums.PlotAttributes.X.value] for fee in fees],\n            y=cumulative_fees,\n            title=f\"{currency} paid funding fees\",\n            own_yaxis=own_yaxis,\n            line_shape=\"hv\")\n\n\ndef _position_factory(symbol, contract_data):\n    # TODO: historical unrealized pnl, maybe find a better solution that this\n    import mock\n    class _TraderMock:\n        def __init__(self):\n            self.exchange_manager = mock.Mock()\n            self.simulate = True\n\n    contract = trading_exchange_data.FutureContract(\n        symbol,\n        trading_enums.MarginType(contract_data[\"margin_type\"]),\n        trading_enums.FutureContractType(contract_data[\"contract_type\"])\n    )\n    return trading_personal_data.create_position_from_type(_TraderMock(), contract)\n\n\ndef _evaluate_portfolio(portfolio, price_data, use_start_value):\n    handled_currencies = []\n    value = 0\n\n    vals = {}\n    for pair, candles in price_data.items():\n        candle = candles[0 if use_start_value else len(candles) - 1]\n        symbol, ref_market = symbol_util.parse_symbol(pair).base_and_quote()\n        if symbol not in handled_currencies:\n            value += portfolio.get(symbol, {}).get(octobot_commons.constants.PORTFOLIO_TOTAL, 0) * candle[\n                commons_enums.PriceIndexes.IND_PRICE_OPEN.value\n            ]\n            vals[symbol] = candle[\n                commons_enums.PriceIndexes.IND_PRICE_OPEN.value\n            ]\n            handled_currencies.append(symbol)\n        if ref_market not in handled_currencies:\n            value += portfolio.get(ref_market, {}).get(octobot_commons.constants.PORTFOLIO_TOTAL, 0)\n            handled_currencies.append(ref_market)\n    return value\n\n\nasync def get_portfolio_values(meta_database, exchange=None, historical_values=None):\n    price_data, trades_data, moving_portfolio_data, trading_type, metadata, _ = \\\n        historical_values or await load_historical_values(meta_database, exchange, with_portfolio=False, with_trades=False)\n    starting_portfolio = json.loads(metadata[commons_enums.BacktestingMetadata.START_PORTFOLIO.value].replace(\"'\", '\"'))\n    ending_portfolio = json.loads(metadata[commons_enums.BacktestingMetadata.END_PORTFOLIO.value].replace(\"'\", '\"'))\n    return _evaluate_portfolio(\n        starting_portfolio,\n        price_data,\n        True,\n    ), _evaluate_portfolio(\n        ending_portfolio,\n        price_data,\n        False,\n    )\n\n\nasync def plot_historical_portfolio_value(\n    meta_database, plotted_element, exchange=None, own_yaxis=False, historical_values=None\n):\n    price_data, trades_data, moving_portfolio_data, trading_type, metadata, _ = \\\n        historical_values or await load_historical_values(meta_database, exchange)\n    price_data_by_time = {}\n    for symbol, candles in price_data.items():\n        price_data_by_time[symbol] = {\n            candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]: candle\n            for candle in candles\n        }\n    if trading_type == \"future\":\n        # TODO: historical unrealized pnl\n        pass\n    for pair in trades_data:\n        trades_data[pair] = sorted(trades_data[pair], key=lambda tr: tr[commons_enums.PlotAttributes.X.value])\n    funding_fees_history_by_pair = await _get_grouped_funding_fees(meta_database,\n                                                                   commons_enums.DBRows.SYMBOL.value)\n    value_data = sortedcontainers.SortedDict()\n    pairs = list(trades_data)\n    if pairs:\n        pair = pairs[0]\n        candles = price_data_by_time[pair]\n        value_data = sortedcontainers.SortedDict({\n            t: 0\n            for t in candles\n        })\n        trade_index_by_pair = {p: 0 for p in pairs}\n        funding_fees_index_by_pair = {p: 0 for p in pairs}\n        # TODO multi exchanges\n        exchange_name = metadata[commons_enums.DBRows.EXCHANGES.value][0]\n        # TODO hedge mode with multi position by pair\n        # if metadata[commons_enums.DBRows.FUTURE_CONTRACTS.value] and \\\n        #         exchange_name in metadata[commons_enums.DBRows.FUTURE_CONTRACTS.value]:\n        #     positions_by_pair = {\n        #         pair: _position_factory(pair,\n        #                                 metadata[commons_enums.DBRows.FUTURE_CONTRACTS.value][exchange_name][pair])\n        #         for pair in pairs\n        #     }\n        # else:\n        #     positions_by_pair = {}\n        # TODO update position instead of portfolio when filled orders and apply position unrealized pnl to portfolio\n        for candle_time, ref_candle in candles.items():\n            current_candles = {}\n            for pair in pairs:\n                if candle_time not in price_data_by_time[pair]:\n                    # no price data for this time in this pair\n                    continue\n                other_candle = price_data_by_time[pair][candle_time]\n                current_candles[pair] = other_candle\n                symbol, ref_market = symbol_util.parse_symbol(pair).base_and_quote()\n                moving_portfolio_data[ref_market] = moving_portfolio_data.get(ref_market, 0)\n                moving_portfolio_data[symbol] = moving_portfolio_data.get(symbol, 0)\n                # part 1: compute portfolio total value after trade update when any\n                # 1.1: trades\n                # start iteration where it last stopped to reduce complexity\n                for trade_index, trade in enumerate(trades_data[pair][trade_index_by_pair[pair]:]):\n                    # handle trades that are both older and at the current candle starting from the last trade index\n                    # (older trades to handle the ones that might be from candles we dont have data on)\n                    if trade[commons_enums.PlotAttributes.X.value] <= candle_time:\n                        if trade[commons_enums.PlotAttributes.SIDE.value] == trading_enums.TradeOrderSide.SELL.value:\n                            moving_portfolio_data[symbol] -= trade[commons_enums.PlotAttributes.VOLUME.value]\n                            moving_portfolio_data[ref_market] += trade[commons_enums.PlotAttributes.VOLUME.value] * \\\n                                                                 trade[commons_enums.PlotAttributes.Y.value]\n                        else:\n                            moving_portfolio_data[symbol] += trade[commons_enums.PlotAttributes.VOLUME.value]\n                            moving_portfolio_data[ref_market] -= trade[commons_enums.PlotAttributes.VOLUME.value] * \\\n                                                                 trade[commons_enums.PlotAttributes.Y.value]\n                        moving_portfolio_data[trade[commons_enums.DBRows.FEES_CURRENCY.value]] -= \\\n                            trade[commons_enums.DBRows.FEES_AMOUNT.value]\n\n                        # last trade case: as there is not trade afterwards, the next condition would never be filled,\n                        # force trade_index_by_pair[pair] increment\n                        if all(it_trade[commons_enums.PlotAttributes.X.value] ==\n                               trade[commons_enums.PlotAttributes.X.value]\n                               for it_trade in trades_data[pair][trade_index_by_pair[pair]:]):\n                            trade_index_by_pair[pair] += 1\n                            break\n\n                    if trade[commons_enums.PlotAttributes.X.value] > \\\n                            ref_candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]:\n                        # no need to continue iterating, save current index for new candle\n                        trade_index_by_pair[pair] += trade_index\n                        break\n                # 1.2: funding fees\n                # start iteration where it last stopped to reduce complexity\n                for funding_fee_index, funding_fee \\\n                        in enumerate(funding_fees_history_by_pair.get(pair, [])[funding_fees_index_by_pair[pair]:]):\n                    if funding_fee[commons_enums.PlotAttributes.X.value] == candle_time:\n                        moving_portfolio_data[funding_fee[trading_enums.FeePropertyColumns.CURRENCY.value]] -= \\\n                            funding_fee[\"quantity\"]\n                    if funding_fee[commons_enums.PlotAttributes.X.value] > candle_time:\n                        # no need to continue iterating, save current index for new candle\n                        funding_fees_index_by_pair[pair] = funding_fee_index  # TODO\n                        break\n            # part 2: now that portfolio is up-to-date, compute portfolio total value\n            handled_currencies = []\n            for pair, other_candle in current_candles.items():\n                symbol, ref_market = symbol_util.parse_symbol(pair).base_and_quote()\n                if symbol not in handled_currencies:\n                    value_data[candle_time] = \\\n                        value_data[candle_time] + \\\n                        moving_portfolio_data[symbol] * other_candle[\n                            commons_enums.PriceIndexes.IND_PRICE_OPEN.value\n                        ]\n                    handled_currencies.append(symbol)\n                if ref_market not in handled_currencies:\n                    value_data[candle_time] = value_data[candle_time] + moving_portfolio_data[ref_market]\n                    handled_currencies.append(ref_market)\n    plotted_element.plot(\n        mode=\"scatter\",\n        x=list(value_data.keys()),\n        y=list(value_data.values()),\n        title=\"Portfolio value\",\n        own_yaxis=own_yaxis\n    )\n\n\ndef _read_pnl_from_trades(x_data, pnl_data, cumulative_pnl_data, trades_history, x_as_trade_count):\n    buy_order_volume_by_price_by_currency = {\n        symbol_util.parse_symbol(symbol).base: {}\n        for symbol in trades_history.keys()\n    }\n    all_trades = []\n    buy_fees = 0\n    sell_fees = 0\n    for trades in trades_history.values():\n        all_trades += trades\n    for trade in sorted(all_trades, key=lambda x: x[commons_enums.PlotAttributes.X.value]):\n        currency, ref_market = symbol_util.parse_symbol(trade[commons_enums.DBRows.SYMBOL.value]).base_and_quote()\n        trade_volume = trade[commons_enums.PlotAttributes.VOLUME.value]\n        buy_order_volume_by_price = buy_order_volume_by_price_by_currency[currency]\n        if trade[commons_enums.PlotAttributes.SIDE.value] == trading_enums.TradeOrderSide.BUY.value:\n            fees = trade[commons_enums.DBRows.FEES_AMOUNT.value]\n            fees_multiplier = 1 if trade[commons_enums.DBRows.FEES_CURRENCY.value] == currency \\\n                else 1 / trade[commons_enums.PlotAttributes.Y.value]\n            paid_fees = fees * fees_multiplier\n            buy_fees += paid_fees * trade[commons_enums.PlotAttributes.Y.value]\n            buy_cost = trade_volume * trade[commons_enums.PlotAttributes.Y.value]\n            if trade[commons_enums.PlotAttributes.Y.value] in buy_order_volume_by_price:\n                buy_order_volume_by_price[buy_cost / (trade_volume - paid_fees)] += trade_volume - paid_fees\n            else:\n                buy_order_volume_by_price[buy_cost / (trade_volume - paid_fees)] = trade_volume - paid_fees\n        elif trade[commons_enums.PlotAttributes.SIDE.value] == trading_enums.TradeOrderSide.SELL.value:\n            remaining_sell_volume = trade_volume\n            volume_by_bought_prices = {}\n            for order_price in list(buy_order_volume_by_price.keys()):\n                if buy_order_volume_by_price[order_price] > remaining_sell_volume:\n                    buy_order_volume_by_price[order_price] -= remaining_sell_volume\n                    volume_by_bought_prices[order_price] = remaining_sell_volume\n                    remaining_sell_volume = 0\n                elif buy_order_volume_by_price[order_price] == remaining_sell_volume:\n                    buy_order_volume_by_price.pop(order_price)\n                    volume_by_bought_prices[order_price] = remaining_sell_volume\n                    remaining_sell_volume = 0\n                else:\n                    # buy_order_volume_by_price[order_price] < remaining_sell_volume\n                    buy_volume = buy_order_volume_by_price.pop(order_price)\n                    volume_by_bought_prices[order_price] = buy_volume\n                    remaining_sell_volume -= buy_volume\n                if remaining_sell_volume <= 0:\n                    break\n            if volume_by_bought_prices:\n                # use total_bought_volume only to avoid taking pre-existing open positions into account\n                # (ex if started with already 10 btc)\n                # total obtained (in ref market) – sell order fees – buy costs (in ref market before fees)\n                buy_cost = sum(price * volume for price, volume in volume_by_bought_prices.items())\n                fees = trade[commons_enums.DBRows.FEES_AMOUNT.value]\n                fees_multiplier = 1 if trade[commons_enums.DBRows.FEES_CURRENCY.value] == ref_market \\\n                    else trade[commons_enums.PlotAttributes.Y.value]\n                sell_fees += fees * fees_multiplier\n                local_pnl = trade[commons_enums.PlotAttributes.Y.value] * \\\n                            trade_volume - (fees * fees_multiplier) - buy_cost\n                pnl_data.append(local_pnl)\n                cumulative_pnl_data.append(local_pnl + cumulative_pnl_data[-1])\n                if x_as_trade_count:\n                    x_data.append(len(pnl_data) - 1)\n                else:\n                    x_data.append(trade[commons_enums.PlotAttributes.X.value])\n        else:\n            get_logger().error(f\"Unknown trade side: {trade}\")\n\n\ndef _read_pnl_from_transactions(x_data, pnl_data, cumulative_pnl_data, trading_transactions_history, x_as_trade_count):\n    previous_value = 0\n    for transaction in trading_transactions_history:\n        transaction_pnl = 0 if transaction[\"realised_pnl\"] is None else transaction[\"realised_pnl\"]\n        transaction_quantity = 0 if transaction[\"quantity\"] is None else transaction[\"quantity\"]\n        local_quantity = transaction_pnl + transaction_quantity\n        cumulated_pnl = local_quantity + previous_value\n        pnl_data.append(local_quantity)\n        cumulative_pnl_data.append(cumulated_pnl)\n        previous_value = cumulated_pnl\n        if x_as_trade_count:\n            x_data.append(len(pnl_data) - 1)\n        else:\n            x_data.append(transaction[commons_enums.PlotAttributes.X.value])\n\n\nasync def _get_historical_pnl(meta_database, plotted_element, include_cumulative, include_unitary,\n                              exchange=None, x_as_trade_count=True, own_yaxis=False, historical_values=None):\n    # PNL:\n    # 1. open position: consider position opening fee from PNL\n    # 2. close position: consider closed amount + closing fee into PNL\n    # what is a trade ?\n    #   futures: when position going to 0 (from long/short) => trade is closed\n    #   spot: when position lowered => trade is closed\n    price_data, trades_data, _, _, _, _ = historical_values or await load_historical_values(meta_database, exchange)\n    if not (price_data and next(iter(price_data.values()))):\n        return\n    x_data = [0 if x_as_trade_count\n              else next(iter(price_data.values()))[0][commons_enums.PriceIndexes.IND_PRICE_TIME.value]]\n    pnl_data = [0]\n    cumulative_pnl_data = [0]\n    trading_transactions_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.TRADING_FEE.value,\n                           trading_enums.TransactionType.FUNDING_FEE.value,\n                           trading_enums.TransactionType.REALISED_PNL.value,\n                           trading_enums.TransactionType.CLOSE_REALISED_PNL.value)\n    )\n    if trading_transactions_history:\n        # can rely on pnl history\n        _read_pnl_from_transactions(x_data, pnl_data, cumulative_pnl_data,\n                                    trading_transactions_history, x_as_trade_count)\n    else:\n        # recreate pnl history from trades\n        _read_pnl_from_trades(x_data, pnl_data, cumulative_pnl_data, trades_data, x_as_trade_count)\n\n    if not x_as_trade_count:\n        # x axis is time: add a value at the end of the axis if missing to avoid a missing values at the end feeling\n        last_time_value = next(iter(price_data.values()))[-1][commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n        if x_data[-1] != last_time_value:\n            # append the latest value at the end of the x axis\n            x_data.append(last_time_value)\n            pnl_data.append(0)\n            cumulative_pnl_data.append(cumulative_pnl_data[-1])\n\n    if include_unitary:\n        plotted_element.plot(\n            kind=\"bar\",\n            x=x_data,\n            y=pnl_data,\n            x_type=\"tick0\" if x_as_trade_count else \"date\",\n            title=\"P&L per trade\",\n            own_yaxis=own_yaxis)\n\n    if include_cumulative:\n        plotted_element.plot(\n            mode=\"scatter\",\n            x=x_data,\n            y=cumulative_pnl_data,\n            x_type=\"tick0\" if x_as_trade_count else \"date\",\n            title=\"Cumulative P&L\",\n            own_yaxis=own_yaxis,\n            line_shape=\"hv\")\n\n\nasync def total_paid_fees(meta_database, all_trades):\n    paid_fees = 0\n    fees_currency = None\n    trading_transactions_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.FUNDING_FEE.value,)\n    )\n    if trading_transactions_history:\n        for transaction in trading_transactions_history:\n            if fees_currency is None:\n                fees_currency = transaction[\"currency\"]\n            if transaction[\"currency\"] != fees_currency:\n                get_logger().error(f\"Unknown funding fee value: {transaction}\")\n            else:\n                # - because funding fees are stored as negative number when paid (positive when \"gained\")\n                paid_fees -= transaction[\"quantity\"]\n    for trade in all_trades:\n        currency = symbol_util.parse_symbol(trade[commons_enums.DBRows.SYMBOL.value]).base\n        if trade[commons_enums.DBRows.FEES_CURRENCY.value] == currency:\n            if trade[commons_enums.DBRows.FEES_CURRENCY.value] == fees_currency:\n                paid_fees += trade[commons_enums.DBRows.FEES_AMOUNT.value]\n            else:\n                paid_fees += trade[commons_enums.DBRows.FEES_AMOUNT.value] * \\\n                             trade[commons_enums.PlotAttributes.Y.value]\n        else:\n            if trade[commons_enums.DBRows.FEES_CURRENCY.value] == fees_currency:\n                paid_fees += trade[commons_enums.DBRows.FEES_AMOUNT.value] / \\\n                             trade[commons_enums.PlotAttributes.Y.value]\n            else:\n                paid_fees += trade[commons_enums.DBRows.FEES_AMOUNT.value]\n    return paid_fees\n\n\nasync def plot_historical_pnl_value(meta_database, plotted_element, exchange=None, x_as_trade_count=True,\n                                    own_yaxis=False, include_cumulative=True, include_unitary=True,\n                                    historical_values=None):\n    return await _get_historical_pnl(meta_database, plotted_element, include_cumulative, include_unitary,\n                                     exchange=exchange, x_as_trade_count=x_as_trade_count, own_yaxis=own_yaxis,\n                                     historical_values=historical_values)\n\n\ndef _plot_table_data(data, plotted_element, data_name, additional_key_to_label, additional_columns,\n                     datum_columns_callback):\n    if not data:\n        get_logger().debug(f\"Nothing to create a table from when reading {data_name}\")\n        return\n    column_render = _get_default_column_render()\n    types = _get_default_types()\n    key_to_label = {\n        **plotted_element.TABLE_KEY_TO_COLUMN,\n        **additional_key_to_label\n    }\n    columns = _get_default_columns(plotted_element, data, column_render, key_to_label) + additional_columns\n    if datum_columns_callback:\n        for datum in data:\n            datum_columns_callback(datum)\n    rows = _get_default_rows(data, columns)\n    searches = _get_default_searches(columns, types)\n    plotted_element.table(\n        data_name,\n        columns=columns,\n        rows=rows,\n        searches=searches\n    )\n\n\nasync def plot_trades(meta_database, plotted_element, historical_values=None):\n    if historical_values:\n        _, trades_data, _, _, _, _ = historical_values\n        data = []\n        for trades in trades_data.values():\n            data += trades\n    else:\n        account_type = trading_api.get_account_type_from_run_metadata(await get_metadata(meta_database))\n        data = await meta_database.get_trades_db(account_type).all(commons_enums.DBTables.TRADES.value)\n    key_to_label = {\n        commons_enums.PlotAttributes.Y.value: \"Price\",\n        commons_enums.PlotAttributes.TYPE.value: \"Type\",\n        commons_enums.PlotAttributes.SIDE.value: \"Side\",\n    }\n    additional_columns = [\n        {\n            \"field\": \"total\",\n            \"label\": \"Total\",\n            \"render\": None\n        }, {\n            \"field\": \"fees\",\n            \"label\": \"Fees\",\n            \"render\": None\n        }\n    ]\n\n    def datum_columns_callback(datum):\n        datum[\"total\"] = datum[\"cost\"]\n        datum[\"fees\"] = f'{datum[\"fees_amount\"]} {datum[\"fees_currency\"]}'\n\n    _plot_table_data(data, plotted_element, commons_enums.DBTables.TRADES.value,\n                     key_to_label, additional_columns, datum_columns_callback)\n\n\nasync def plot_orders(meta_database, plotted_element, historical_values=None):\n    if historical_values:\n        _, _, _, _, metadata, _ = historical_values\n    else:\n        metadata = await get_metadata(meta_database)\n    account_type = trading_api.get_account_type_from_run_metadata(metadata)\n    data = [\n        order[trading_constants.STORAGE_ORIGIN_VALUE]\n        for order in await meta_database.get_orders_db(account_type).all(commons_enums.DBTables.ORDERS.value)\n    ]\n    key_to_label = {\n        trading_enums.ExchangeConstantsOrderColumns.TIMESTAMP.value: \"Time\",\n        trading_enums.ExchangeConstantsOrderColumns.PRICE.value: \"Price\",\n        trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value: \"Amount\",\n        trading_enums.ExchangeConstantsOrderColumns.TYPE.value: \"Type\",\n        trading_enums.ExchangeConstantsOrderColumns.SIDE.value: \"Side\",\n    }\n    additional_columns = [\n        {\n            \"field\": \"total\",\n            \"label\": \"Total\",\n            \"render\": None\n        }\n    ]\n\n    def datum_columns_callback(datum):\n        datum[\"total\"] = datum[trading_enums.ExchangeConstantsOrderColumns.COST.value]\n        datum[trading_enums.ExchangeConstantsOrderColumns.TIMESTAMP.value] *= 1000\n\n    _plot_table_data(data, plotted_element, commons_enums.DBTables.ORDERS.value,\n                     key_to_label, additional_columns, datum_columns_callback)\n\n\nasync def plot_withdrawals(meta_database, plotted_element):\n    withdrawal_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.BLOCKCHAIN_WITHDRAWAL.value,)\n    )\n    # apply quantity to y for each withdrawal\n    for withdrawal in withdrawal_history:\n        withdrawal[commons_enums.PlotAttributes.Y.value] = withdrawal[\"quantity\"]\n    key_to_label = {\n        commons_enums.PlotAttributes.Y.value: \"Quantity\",\n        \"currency\": \"Currency\",\n        commons_enums.PlotAttributes.SIDE.value: \"Side\",\n    }\n    additional_columns = []\n\n    _plot_table_data(withdrawal_history, plotted_element, \"Withdrawals\",\n                     key_to_label, additional_columns, None)\n\n\nasync def plot_positions(meta_database, plotted_element):\n    realized_pnl_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.CLOSE_REALISED_PNL.value,)\n    )\n    key_to_label = {\n        commons_enums.PlotAttributes.X.value: \"Exit time\",\n        \"first_entry_time\": \"Entry time\",\n        \"average_entry_price\": \"Average entry price\",\n        \"average_exit_price\": \"Average exit price\",\n        \"cumulated_closed_quantity\": \"Cumulated closed quantity\",\n        \"realised_pnl\": \"Realised PNL\",\n        commons_enums.PlotAttributes.SIDE.value: \"Side\",\n        \"trigger_source\": \"Closed by\",\n    }\n\n    _plot_table_data(realized_pnl_history, plotted_element, \"Positions\", key_to_label, [], None)\n\n\nasync def display(plotted_element, label, value):\n    plotted_element.value(label, value)\n\n\nasync def display_html(plotted_element, html):\n    plotted_element.html_value(html)\n\n\nasync def plot_table(meta_database, plotted_element, data_source, columns=None, rows=None,\n                     searches=None, column_render=None, types=None, cache_value=None):\n    data = []\n    metadata = await get_metadata(meta_database)\n    account_type = trading_api.get_account_type_from_run_metadata(metadata)\n    if data_source == commons_enums.DBTables.TRADES.value:\n        data = await meta_database.get_trades_db(account_type).all(commons_enums.DBTables.TRADES.value)\n    elif data_source == commons_enums.DBTables.ORDERS.value:\n        data = await meta_database.get_orders_db(account_type).all(commons_enums.DBTables.ORDERS.value)\n    else:\n        exchange = meta_database.run_dbs_identifier.context.exchange_name\n        symbol = meta_database.run_dbs_identifier.context.symbol\n        symbol_db = meta_database.get_symbol_db(exchange, symbol)\n        if cache_value is None:\n            data = await symbol_db.all(data_source)\n        else:\n            query = (await symbol_db.search()).title == data_source\n            cache_data = await symbol_db.select(commons_enums.DBTables.CACHE_SOURCE.value, query)\n            if cache_data:\n                try:\n                    cache_database = databases.CacheDatabase(cache_data[0][commons_enums.PlotAttributes.VALUE.value])\n                    cache = await cache_database.get_cache()\n                    x_shift = cache_data[0][\"x_shift\"]\n                    data = [\n                        {\n                            commons_enums.PlotAttributes.X.value: (cache_element[commons_enums.CacheDatabaseColumns.TIMESTAMP.value] + x_shift) * 1000,\n                            commons_enums.PlotAttributes.Y.value: cache_element[cache_value]\n                        }\n                        for cache_element in cache\n                    ]\n                except KeyError as e:\n                    get_logger().warning(f\"Missing cache values when plotting data: {e}\")\n                except commons_errors.DatabaseNotFoundError as e:\n                    get_logger().warning(f\"Missing cache values when plotting data: {e}\")\n\n    if not data:\n        get_logger().debug(f\"Nothing to create a table from when reading {data_source}\")\n        return\n    column_render = column_render or _get_default_column_render()\n    types = types or _get_default_types()\n    columns = columns or _get_default_columns(plotted_element, data, column_render)\n    rows = rows or _get_default_rows(data, columns)\n    searches = searches or _get_default_searches(columns, types)\n    plotted_element.table(\n        data_source,\n        columns=columns,\n        rows=rows,\n        searches=searches)\n\n\ndef _get_default_column_render():\n    return {\n        \"Time\": \"datetime\",\n        \"Entry time\": \"datetime\",\n        \"Exit time\": \"datetime\"\n    }\n\n\ndef _get_default_types():\n    return {\n        \"Time\": \"datetime\",\n        \"Entry time\": \"datetime\",\n        \"Exit time\": \"datetime\"\n    }\n\n\ndef _get_default_columns(plotted_element, data, column_render, key_to_label=None):\n    key_to_label = key_to_label or plotted_element.TABLE_KEY_TO_COLUMN\n    return [\n        {\n            \"field\": row_key,\n            \"label\": key_to_label[row_key],\n            \"render\": column_render.get(key_to_label[row_key], None)\n        }\n        for row_key, row_value in data[0].items()\n        if row_key in key_to_label and row_value is not None\n    ]\n\n\ndef _get_default_rows(data, columns):\n    column_fields = set(col[\"field\"] for col in columns)\n    return [\n        {key: val for key, val in row.items() if key in column_fields}\n        for row in data\n    ]\n\n\ndef _get_default_searches(columns, types):\n    return [\n        {\n            \"field\": col[\"field\"],\n            \"label\": col[\"label\"],\n            \"type\": types.get(col[\"label\"])\n        }\n        for col in columns\n    ]\n\n\ndef _get_wins_and_losses_from_transactions(x_data, wins_and_losses_data, trading_transactions_history,\n                                           x_as_trade_count):\n    for transaction in trading_transactions_history:\n        transaction_pnl = 0 if transaction[\"realised_pnl\"] is None else transaction[\"realised_pnl\"]\n        current_cumulative_wins = wins_and_losses_data[-1] if wins_and_losses_data else 0\n        if transaction_pnl < 0:\n            wins_and_losses_data.append(current_cumulative_wins - 1)\n        elif transaction_pnl > 0:\n            wins_and_losses_data.append(current_cumulative_wins + 1)\n        else:\n            continue\n\n        if x_as_trade_count:\n            x_data.append(len(wins_and_losses_data) - 1)\n        else:\n            x_data.append(transaction[commons_enums.PlotAttributes.X.value])\n\n\ndef _get_wins_and_losses_from_trades(x_data, wins_and_losses_data, trades_history, x_as_trade_count):\n    # todo\n    pass\n\n\nasync def plot_historical_wins_and_losses(meta_database, plotted_element, exchange=None, x_as_trade_count=False,\n                                          own_yaxis=True, historical_values=None):\n    price_data, trades_data, _, _, _, _ = historical_values or await load_historical_values(meta_database, exchange)\n    if not (price_data and next(iter(price_data.values()))):\n        return\n    x_data = []\n    wins_and_losses_data = []\n    trading_transactions_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.TRADING_FEE.value,\n                           trading_enums.TransactionType.FUNDING_FEE.value,\n                           trading_enums.TransactionType.REALISED_PNL.value,\n                           trading_enums.TransactionType.CLOSE_REALISED_PNL.value)\n    )\n    if trading_transactions_history:\n        # can rely on pnl history\n        _get_wins_and_losses_from_transactions(x_data, wins_and_losses_data,\n                                               trading_transactions_history, x_as_trade_count)\n    else:\n        # recreate pnl history from trades\n        return  # todo not implemented yet\n        # _read_pnl_from_trades(x_data, pnl_data, cumulative_pnl_data, trades_data, x_as_trade_count)\n\n    plotted_element.plot(\n        mode=\"scatter\",\n        x=x_data,\n        y=wins_and_losses_data,\n        x_type=\"tick0\" if x_as_trade_count else \"date\",\n        title=\"wins and losses count\",\n        own_yaxis=own_yaxis,\n        line_shape=\"hv\")\n\n\ndef _get_win_rates_from_transactions(x_data, win_rates_data, trading_transactions_history,\n                                     x_as_trade_count):\n    wins_count = 0\n    losses_count = 0\n    for transaction in trading_transactions_history:\n        transaction_pnl = 0 if transaction[\"realised_pnl\"] is None else transaction[\"realised_pnl\"]\n        if transaction_pnl < 0:\n            losses_count += 1\n        elif transaction_pnl > 0:\n            wins_count += 1\n        else:\n            continue\n\n        win_rates_data.append((wins_count/(losses_count+wins_count))*100)\n        if x_as_trade_count:\n            x_data.append(len(win_rates_data) - 1)\n        else:\n            x_data.append(transaction[commons_enums.PlotAttributes.X.value])\n\n\ndef _get_win_rates_from_trades(x_data, win_rates_data, trades_history, x_as_trade_count):\n    # todo\n    pass\n\n\nasync def plot_historical_win_rates(meta_database, plotted_element, exchange=None,\n                                    x_as_trade_count=False, own_yaxis=True, historical_values=None):\n    price_data, trades_data, _, _, _, _ = historical_values or await load_historical_values(meta_database, exchange)\n    if not (price_data and next(iter(price_data.values()))):\n        return\n    x_data = []\n    win_rates_data = []\n    trading_transactions_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.TRADING_FEE.value,\n                           trading_enums.TransactionType.FUNDING_FEE.value,\n                           trading_enums.TransactionType.REALISED_PNL.value,\n                           trading_enums.TransactionType.CLOSE_REALISED_PNL.value)\n    )\n    if trading_transactions_history:\n        # can rely on pnl history\n        _get_win_rates_from_transactions(x_data, win_rates_data,\n                                         trading_transactions_history, x_as_trade_count)\n    else:\n        # recreate pnl history from trades\n        return  # todo not implemented yet\n        # _get_win_rates_from_trades(x_data, pnl_data, cumulative_pnl_data, trades_data, x_as_trade_count)\n\n    plotted_element.plot(\n        mode=\"scatter\",\n        x=x_data,\n        y=win_rates_data,\n        x_type=\"tick0\" if x_as_trade_count else \"date\",\n        title=\"win rate\",\n        own_yaxis=own_yaxis,\n        line_shape=\"hv\")\n\n\nasync def _get_best_case_growth_from_transactions(trading_transactions_history,\n                                                  x_as_trade_count, meta_database):\n    ref_market = meta_database.run_db._database.adaptor.database.storage.cache[commons_enums.DBTables.METADATA.value]['1']['ref_market']\n    start_balance = meta_database.run_db._database.adaptor.database.storage.cache[commons_enums.DBTables.PORTFOLIO.value]['1'][ref_market]['total']\n    best_case_data, _, start_balance, end_balance, x_data \\\n        = await portfolio_util.get_coefficient_of_determination_data(transactions=trading_transactions_history,\n                                                                     start_balance=start_balance,\n                                                                     use_high_instead_of_end_balance=True,\n                                                                     x_as_trade_count=x_as_trade_count)\n    if best_case_data:\n        return x_data, best_case_data\n    return [], []\n\n\nasync def plot_best_case_growth(meta_database, plotted_element, exchange=None,\n                                x_as_trade_count=False, own_yaxis=False, historical_values=None):\n    price_data, trades_data, _, _, _, _ = historical_values or await load_historical_values(meta_database, exchange)\n    if not (price_data and next(iter(price_data.values()))):\n        return\n    x_data = []\n    best_case_data = []\n    trading_transactions_history = await get_transactions(\n        meta_database,\n        transaction_types=(trading_enums.TransactionType.TRADING_FEE.value,\n                           trading_enums.TransactionType.FUNDING_FEE.value,\n                           trading_enums.TransactionType.REALISED_PNL.value,\n                           trading_enums.TransactionType.CLOSE_REALISED_PNL.value)\n    )\n    if trading_transactions_history:\n        # can rely on pnl history\n        x_data, best_case_data = await _get_best_case_growth_from_transactions(trading_transactions_history,\n                                                                               x_as_trade_count, meta_database)\n\n    plotted_element.plot(\n        mode=\"scatter\",\n        x=x_data,\n        y=best_case_data,\n        x_type=\"tick0\" if x_as_trade_count else \"date\",\n        title=\"best case growth\",\n        own_yaxis=own_yaxis,\n        line_shape=\"hv\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/configuration/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom tentacles.Meta.Keywords.scripting_library.configuration.profile_data_configuration import *\nfrom tentacles.Meta.Keywords.scripting_library.configuration.tentacles_configuration import *\nfrom tentacles.Meta.Keywords.scripting_library.configuration.indexes_configuration import *\nfrom tentacles.Meta.Keywords.scripting_library.configuration.exchanges_configuration import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/configuration/exchanges_configuration.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.constants as constants\n\n\n# TODO later: find a way to store this in exchange tentacles instead and use exchange.get_default_reference_market\n# Issue: hollaex based exchanages require an exchange configuration to be identified as such \n_SPECIFIC_REFERENCE_MARKET_PER_EXCHANGE: dict[str, str] = {\n    \"coinbase\": \"USDC\",\n    \"binance\": \"USDC\",\n}\n_EXCHANGES_WITH_DIFFERENT_PUBLIC_DATA_AFTER_AUTH = set[str]([\n    \"mexc\",\n    \"lbank\",\n])\n\ndef get_default_reference_market_per_exchange(exchanges: list[str]) -> dict[str, str]:\n    return {exchange: get_default_exchange_reference_market(exchange) for exchange in exchanges}\n\ndef get_default_exchange_reference_market(exchange: str) -> str:\n    return _SPECIFIC_REFERENCE_MARKET_PER_EXCHANGE.get(exchange, constants.DEFAULT_REFERENCE_MARKET)\n\ndef is_exchange_with_different_public_data_after_auth(exchange: str) -> bool:\n    return exchange in _EXCHANGES_WITH_DIFFERENT_PUBLIC_DATA_AFTER_AUTH\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/configuration/indexes_configuration.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\nimport octobot_commons\nimport octobot_commons.constants as common_constants\nimport octobot_commons.enums as common_enums\nimport octobot_commons.profiles as commons_profiles\nimport octobot_commons.profiles.profile_data as commons_profile_data\nimport octobot_commons.symbols\n\nimport octobot_evaluators.constants as evaluators_constants\n\nimport octobot_trading.constants as trading_constants\n\nimport tentacles.Trading.Mode.index_trading_mode.index_trading as index_trading\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\nimport tentacles.Meta.Keywords.scripting_library.configuration.exchanges_configuration as exchanges_configuration\n\n\ndef create_index_config_from_tentacles_config(\n    tentacles_config: list[commons_profile_data.TentaclesData], exchange: str,\n    starting_funds: float, backtesting_start_time_delta: float\n) -> commons_profiles.ProfileData:\n    trading_mode_config = tentacles_config[0].config\n    distribution = trading_mode_config[index_trading.IndexTradingModeProducer.INDEX_CONTENT]\n    reference_market = exchanges_configuration.get_default_exchange_reference_market(exchange)\n    # replace USD by reference market\n    for element in distribution:\n        if element[index_distribution.DISTRIBUTION_NAME] == \"USD\":\n            element[index_distribution.DISTRIBUTION_NAME] = reference_market\n    coins_by_symbol = {\n        element[index_distribution.DISTRIBUTION_NAME]: element[index_distribution.DISTRIBUTION_NAME]\n        for element in distribution\n    }\n    rebalance_cap = trading_mode_config[index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT]\n    min_funds = starting_funds / 10\n    selected_rebalance_trigger_profile = trading_mode_config.get(index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE, None)\n    rebalance_trigger_profiles = trading_mode_config.get(index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES, None)\n    profile_data_dict = generate_index_config(\n        distribution, rebalance_cap, selected_rebalance_trigger_profile, rebalance_trigger_profiles, reference_market, exchange,\n        min_funds, coins_by_symbol, False, backtesting_start_time_delta\n    )\n    return commons_profiles.ProfileData.from_dict(profile_data_dict)\n\n\ndef generate_index_config(\n    distribution: typing.List, rebalance_cap: float, \n    selected_rebalance_trigger_profile: typing.Optional[str], rebalance_trigger_profiles: typing.Optional[list[dict]], \n    reference_market: str,\n    exchange: str, min_funds: float, coins_by_symbol: dict[str, str], disabled_backtesting: bool,\n    backtesting_start_time_delta: float\n) -> dict:\n    profile_details = commons_profile_data.ProfileDetailsData(name=\"serverless\")\n    trading = commons_profile_data.TradingData(\n        reference_market=reference_market, risk=0.5\n    )\n    config_exchanges = [commons_profile_data.ExchangeData(\n        internal_name=exchange, exchange_type=common_constants.CONFIG_EXCHANGE_SPOT\n    )]\n    currencies = [\n        commons_profile_data.CryptoCurrencyData(\n            [octobot_commons.symbols.merge_currencies(element[index_distribution.DISTRIBUTION_NAME], reference_market)],\n            coins_by_symbol.get(\n                element[index_distribution.DISTRIBUTION_NAME],\n                element[index_distribution.DISTRIBUTION_NAME]\n            )\n        )\n        for element in distribution\n        if element[index_distribution.DISTRIBUTION_NAME] != reference_market\n    ]\n    trader = commons_profile_data.TraderData(enabled=True)\n    trader_simulator = commons_profile_data.TraderSimulatorData()\n    tentacles = [\n        commons_profile_data.TentaclesData(\n            index_trading.IndexTradingMode.get_name(), _get_index_trading_config(\n                distribution, rebalance_cap, selected_rebalance_trigger_profile, rebalance_trigger_profiles\n            )\n        )\n    ]\n    backtesting = generate_index_backtesting_config(\n        exchange, reference_market, min_funds, disabled_backtesting, backtesting_start_time_delta\n    )\n    base_config = commons_profiles.ProfileData(\n        profile_details, currencies, trading, config_exchanges, commons_profile_data.FutureExchangeData(),\n        trader, trader_simulator, tentacles, backtesting\n    )\n    return base_config.to_dict(include_default_values=False)\n\n\ndef generate_index_backtesting_config(\n    exchange: str, reference_market: str, min_funds: float, disabled_backtesting: bool, start_time_delta: float\n) -> commons_profile_data.BacktestingContext:\n    return commons_profile_data.BacktestingContext(\n        exchanges=[] if disabled_backtesting else [exchange],\n        start_time_delta=start_time_delta,\n        starting_portfolio={\n            reference_market: min_funds * 10    # make sure there is always enough funds even if the market crashes\n        }\n    )\n\n\ndef _get_index_trading_config(\n    distribution: typing.List, \n    rebalance_cap: float, \n    selected_rebalance_trigger_profile: typing.Optional[str], \n    rebalance_trigger_profiles: typing.Optional[list[dict]]\n):\n    return {\n        trading_constants.TRADING_MODE_REQUIRED_STRATEGIES: [],\n        index_trading.IndexTradingModeProducer.REFRESH_INTERVAL: 1,\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: rebalance_cap,\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: selected_rebalance_trigger_profile,\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: rebalance_trigger_profiles,\n        index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY: index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE.value,\n        index_trading.IndexTradingModeProducer.SELL_UNINDEXED_TRADED_COINS: True,\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: distribution,\n        evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME: [common_enums.TimeFrames.ONE_DAY.value],\n    }\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/configuration/profile_data_configuration.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport logging\nimport typing\nimport os\nimport sortedcontainers\nimport time\n\nimport octobot_commons\nimport octobot_commons.constants as common_constants\nimport octobot_commons.enums as common_enums\nimport octobot_commons.configuration as commons_configuration\nimport octobot_commons.profiles as commons_profiles\nimport octobot_commons.profiles.profile_data as commons_profile_data\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.symbols\nimport octobot_commons.logging\n\nimport octobot_evaluators.constants as evaluators_constants\n\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.exchange_data as exchange_data_import\nimport octobot_trading.api\n\nimport octobot_tentacles_manager.api\nimport octobot_tentacles_manager.configuration\n\n\nimport tentacles.Trading.Mode.index_trading_mode.index_trading as index_trading\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\nimport tentacles.Meta.Keywords.scripting_library.errors as scr_errors\nimport tentacles.Meta.Keywords.scripting_library.constants as scr_constants\nimport tentacles.Meta.Keywords.scripting_library.configuration.tentacles_configuration as tentacles_configuration\nimport tentacles.Meta.Keywords.scripting_library.configuration.indexes_configuration as indexes_configuration\n\n\n_AUTH_REQUIRED_EXCHANGES: dict[str, bool] = {}\n\n\ndef minimal_profile_data() -> commons_profiles.ProfileData:\n    return commons_profiles.ProfileData.from_dict({\n        \"profile_details\": {\"name\": \"\"},\n        \"crypto_currencies\": [],\n        \"exchanges\": [],\n        \"trading\": {\"reference_market\": common_constants.DEFAULT_REFERENCE_MARKET}\n    })\n\n\ndef empty_config_proxy(*_, **__):\n    return {}\n\n\ndef create_backtesting_config(\n    profile_data: commons_profiles.ProfileData,\n    exchange_data: exchange_data_import.ExchangeData,\n) -> commons_configuration.Configuration:\n    tentacles_config = get_full_tentacles_setup_config(profile_data)\n    apply_leverage_config(profile_data)\n    profile_data.exchanges = [] # clear exchange to avoid conflicts with backtesting exchanges\n    return get_config(\n        profile_data, exchange_data, tentacles_config, False, False, False\n    )\n\n\ndef get_config(\n    profile_data: commons_profiles.ProfileData,\n    exchange_data: exchange_data_import.ExchangeData,\n    tentacles_setup_config,\n    auth: bool,\n    ignore_symbols_in_exchange_init: bool,\n    use_exchange_data_portfolio: bool,\n) -> commons_configuration.Configuration:\n    config = commons_configuration.Configuration(None, None)\n    config.logger.logger.setLevel(logging.WARNING)  # disable \"using XYZ profile.\" log\n    config.config = {}\n    initial_backtesting_context = profile_data.backtesting_context\n    # always use exchange data on real trading\n    # use exchange data on simulated only when exchange_data.portfolio_details.content is available\n    if use_exchange_data_portfolio and (\n        not profile_data.trader_simulator.enabled or exchange_data.portfolio_details.content\n    ):\n        _set_portfolio(profile_data, exchange_data.portfolio_details.content)\n        # do not allow using backtesting context when using exchange data portfolio\n        profile_data.backtesting_context = None # type: ignore\n    profile = profile_data.to_profile(None)\n    profile_data.backtesting_context = initial_backtesting_context\n    config.profile_by_id[profile.profile_id] = profile\n    config.select_profile(profile.profile_id)\n    config.config[common_constants.CONFIG_EXCHANGES][exchange_data.exchange_details.name] = get_exchange_config(\n        exchange_data, tentacles_setup_config, get_config_by_tentacle(profile_data), auth\n    )\n    if ignore_symbols_in_exchange_init:\n        config.config[common_constants.CONFIG_CRYPTO_CURRENCIES] = {}\n    config.config[common_constants.CONFIG_TIME_FRAME] = time_frame_manager.sort_time_frames(list(set(\n        common_enums.TimeFrames(market.time_frame)\n        for market in exchange_data.markets\n    )))\n    return config\n\n\ndef get_exchange_config(\n    exchange_data: exchange_data_import.ExchangeData,\n    tentacles_setup_config,\n    exchange_config_by_exchange: typing.Optional[dict[str, dict]],\n    auth: bool\n):\n    auth_details = exchange_data.auth_details\n    if not auth:\n        always_auth = is_auth_required_exchanges(exchange_data, tentacles_setup_config, exchange_config_by_exchange)\n        if always_auth:\n            auth_details = get_readonly_exchange_auth_details(exchange_data.exchange_details.name)\n            auth = True\n\n    exchange_config = {\n        common_constants.CONFIG_EXCHANGE_KEY: auth_details.api_key if auth else None,\n        common_constants.CONFIG_EXCHANGE_SECRET: auth_details.api_secret if auth else None,\n        common_constants.CONFIG_EXCHANGE_PASSWORD: auth_details.api_password if auth else None,\n        common_constants.CONFIG_EXCHANGE_ACCESS_TOKEN: auth_details.access_token if auth else None,\n        common_constants.CONFIG_EXCHANGE_TYPE: auth_details.exchange_type or common_constants.CONFIG_EXCHANGE_SPOT,\n    }\n    exchange_config[common_constants.CONFIG_EXCHANGE_SANDBOXED] = auth_details.sandboxed\n    return exchange_config\n\n\n\ndef create_profile_data_from_tentacles_config_history(\n    tentacles_config_by_time: dict[float, list[commons_profile_data.TentaclesData]], exchange: str, starting_funds: float\n) -> commons_profiles.ProfileData:\n    if not tentacles_config_by_time:\n        raise ValueError(\"tentacles_config_by_time is empty\")\n    ordered_config = sortedcontainers.SortedDict(tentacles_config_by_time)\n    first_config = next(iter(ordered_config.values()))\n    if first_config[0].name == index_trading.IndexTradingMode.get_name():\n        backtesting_start_time_delta = time.time() - next(iter(ordered_config))\n        historical_config_by_time = {\n            timestamp: indexes_configuration.create_index_config_from_tentacles_config(\n                config, exchange, starting_funds, backtesting_start_time_delta\n            )\n            for timestamp, config in ordered_config.items()\n        }\n        master_config = next(iter(historical_config_by_time.values()))\n        if len(historical_config_by_time) > 1:\n            register_historical_configs(\n                master_config, historical_config_by_time, \n                add_historical_trading_pairs_to_master_profile_data=True, \n                apply_master_tentacle_config_edits_to_historical_configs=False\n            )\n        return master_config\n    else:\n        # todo implement other trading modes if necessary\n        raise ValueError(f\"{first_config.name} config not implemented\")\n\n\n\ndef register_historical_configs(\n    master_profile_data: commons_profiles.ProfileData,\n    historical_profile_data_by_time: dict[float, commons_profiles.ProfileData],\n    add_historical_trading_pairs_to_master_profile_data: bool,\n    apply_master_tentacle_config_edits_to_historical_configs: bool\n):\n    if add_historical_trading_pairs_to_master_profile_data:\n        # 1. register every historical profile traded pairs in master profile\n        if added_pairs := get_historical_added_config_trading_pairs(\n            master_profile_data, historical_profile_data_by_time.values()\n        ):\n            add_traded_symbols(master_profile_data, added_pairs)\n\n    # 2. register historical tentacles_config\n    config_by_tentacle = get_config_by_tentacle(master_profile_data)\n    for historical_time, historical_profile in historical_profile_data_by_time.items():\n        historical_config_by_tentacle = get_config_by_tentacle(historical_profile)\n        for tentacle, config in historical_config_by_tentacle.items():\n            master_config = config_by_tentacle[tentacle]\n            if config is not master_config:\n                if apply_master_tentacle_config_edits_to_historical_configs:\n                    try:\n                        _apply_master_tentacle_config_edits_to_historical_config(tentacle, master_config, config)\n                    except RuntimeError:\n                        # tentacle not found, continue\n                        _get_logger().error(f\"Tentacle {tentacle} not found in available tentacles\")\n                commons_configuration.add_historical_tentacle_config(\n                    master_config,\n                    historical_time,\n                    config,\n                )\n\n\ndef _apply_master_tentacle_config_edits_to_historical_config(tentacle: str, master_config: dict, historical_config: dict):\n    if updatable_keys := tentacles_configuration.get_config_history_propagated_tentacles_config_keys(tentacle):\n        for key in updatable_keys:\n            if key in master_config:\n                historical_config[key] = master_config[key]\n\n\ndef get_historical_added_config_trading_pairs(\n    master_profile_data: commons_profiles.ProfileData, \n    historical_profile_data: typing.Optional[typing.Iterable[commons_profiles.ProfileData]]\n) -> list[str]:\n    if historical_profile_data:\n        historical_pairs = [\n            pair\n            for historical_profile in historical_profile_data\n            for pair in get_traded_symbols(historical_profile)\n        ]\n    else:\n        historical_pairs = get_historical_traded_pairs(master_profile_data)\n    registered_pairs = get_traded_symbols(master_profile_data)\n    added_pairs = []\n    for pair in historical_pairs:\n        if pair not in registered_pairs:\n            registered_pairs.append(pair)\n            added_pairs.append(pair)\n    return added_pairs\n\n\ndef get_historical_traded_pairs(\n    profile_data: commons_profiles.ProfileData\n) -> typing.Iterable[str]:\n    trading_mode = get_trading_mode(profile_data)\n    trading_mode_config = _get_trading_mode_config(profile_data)\n    historical_trading_mode_configs = commons_configuration.get_historical_tentacle_configs(\n        trading_mode_config, 0, time.time()\n    )\n    if trading_mode == index_trading.IndexTradingMode.get_name():\n        return _get_historical_index_trading_pairs(profile_data, historical_trading_mode_configs) #todo\n    else:\n        raise NotImplementedError(f\"Trading mode {trading_mode} not implemented\")\n\n\n\ndef _get_historical_index_trading_pairs(\n    profile_data: commons_profiles.ProfileData, historical_trading_mode_configs: typing.Iterable[dict]\n) -> typing.Iterable[str]:\n    historical_assets = []\n    latest_config_assets = set(\n        asset[index_distribution.DISTRIBUTION_NAME]\n        for asset in _get_trading_mode_config(profile_data)[\n            index_trading.IndexTradingModeProducer.INDEX_CONTENT\n        ]\n    )\n    for historical_trading_mode_config in historical_trading_mode_configs:\n        for asset in historical_trading_mode_config[index_trading.IndexTradingModeProducer.INDEX_CONTENT]:\n            historical_asset = asset[index_distribution.DISTRIBUTION_NAME]\n            if historical_asset not in historical_assets and historical_asset not in latest_config_assets:\n                historical_assets.append(historical_asset)\n    return [\n        octobot_commons.symbols.merge_currencies(asset, profile_data.trading.reference_market)\n        for asset in historical_assets\n    ]\n\n\ndef add_traded_symbols(\n    profile_data: commons_profiles.ProfileData,\n    added_symbols: typing.Iterable[str]\n):\n    traded_symbols = get_traded_symbols(profile_data)\n    to_add_symbols = [\n        symbol\n        for symbol in added_symbols\n        if symbol not in traded_symbols\n    ]\n    if to_add_symbols:\n        _get_logger().info(f\"Adding {to_add_symbols} to profile data traded pairs.\")\n        expand_traded_pairs_into_currencies(profile_data, to_add_symbols)\n\n\ndef expand_traded_pairs_into_currencies(profile_data, pairs: list[str]):\n    for pair in pairs:\n        profile_data.crypto_currencies.append(\n            commons_profile_data.CryptoCurrencyData(\n                trading_pairs=[pair],\n                name=pair,\n                enabled=True\n            )\n        )\n\n\ndef filter_out_missing_symbols(profile_data: commons_profiles.ProfileData, available_symbols: list[str]) -> list[str]:\n    traded_pairs = get_traded_symbols(profile_data)\n    removed_symbols = [symbol for symbol in traded_pairs if symbol not in available_symbols]\n    if removed_symbols:\n        profile_data.crypto_currencies = []\n        add_traded_symbols(\n            profile_data,\n            [pair for pair in traded_pairs if pair not in removed_symbols]\n        )\n    return removed_symbols\n\n\ndef get_readonly_exchange_auth_details(exchange_internal_name: str) -> exchange_data_import.ExchangeAuthDetails:\n    return exchange_data_import.ExchangeAuthDetails(\n        api_key=_get_readonly_exchange_credential_from_env(exchange_internal_name, \"KEY\", False),\n        api_secret=_get_readonly_exchange_credential_from_env(exchange_internal_name, \"SECRET\", False),\n        api_password=_get_readonly_exchange_credential_from_env(exchange_internal_name, \"PASSWORD\", True),\n        sandboxed=False,\n        broker_enabled=False,\n    )\n\n\ndef _get_readonly_exchange_credential_from_env(exchange_name, cred_suffix, allow_missing):\n    # for coinbase: COINBASE_READ_ONLY_KEY, COINBASE_READ_ONLY_SECRET, COINBASE_READ_PASSWORD\n    if cred := os.getenv(f\"{exchange_name}_READ_ONLY_{cred_suffix}\".upper(), None):\n        return commons_configuration.encrypt(cred).decode()\n    if allow_missing:\n        return None\n    raise scr_errors.MissingReadOnlyExchangeCredentialsError(\n        f\"{exchange_name} read only credentials are missing\"\n    )\n\n\ndef is_auth_required_exchanges(\n    exchange_data: exchange_data_import.ExchangeData,\n    tentacles_setup_config,\n    exchange_config_by_exchange: typing.Optional[dict[str, dict]]\n):\n    try:\n        if exchange_config_by_exchange and any(\n            exchange_config.get(common_constants.CONFIG_FORCE_AUTHENTICATION, False)\n            for exchange_config in exchange_config_by_exchange.values()\n        ):\n            # don't use cache when force authentication is True: this can be specific to this context\n            return _get_is_auth_required_exchange(\n                exchange_data, tentacles_setup_config, exchange_config_by_exchange\n            )\n        # use cache to avoid using introspection each time\n        return _AUTH_REQUIRED_EXCHANGES[exchange_data.exchange_details.name]\n    except KeyError:\n        _AUTH_REQUIRED_EXCHANGES[exchange_data.exchange_details.name] = _get_is_auth_required_exchange(\n            exchange_data, tentacles_setup_config, exchange_config_by_exchange\n        )\n        return _AUTH_REQUIRED_EXCHANGES[exchange_data.exchange_details.name]\n\ndef _get_is_auth_required_exchange(\n    exchange_data: exchange_data_import.ExchangeData,\n    tentacles_setup_config,\n    exchange_config_by_exchange: typing.Optional[dict[str, dict]]\n):\n    exchange_class = exchanges.get_rest_exchange_class(\n        exchange_data.exchange_details.name, tentacles_setup_config, exchange_config_by_exchange\n    )\n    return exchange_class.requires_authentication(\n        None, tentacles_setup_config, exchange_config_by_exchange\n    )\n\n\ndef _set_portfolio(\n    profile_data: commons_profiles.ProfileData,\n    portfolio: dict\n):\n    profile_data.trader_simulator.starting_portfolio = get_formatted_portfolio(portfolio)\n\n\ndef get_formatted_portfolio(portfolio: dict):\n    for asset in portfolio.values():\n        if common_constants.PORTFOLIO_AVAILABLE not in asset:\n            asset[common_constants.PORTFOLIO_AVAILABLE] = asset[trading_constants.CONFIG_PORTFOLIO_FREE]\n    return portfolio\n\n\ndef get_config_by_tentacle(profile_data: commons_profiles.ProfileData) -> dict[str, dict]:\n    return {\n        tentacle.name: tentacle.config\n        for tentacle in profile_data.tentacles\n    }\n\n\ndef get_full_tentacles_setup_config(\n    profile_data: commons_profiles.ProfileData = None,\n    ensure_tentacle_info: bool = True,\n    extra_tentacle_names: list = None\n) -> octobot_tentacles_manager.configuration.TentaclesSetupConfiguration:\n    if ensure_tentacle_info:\n        octobot_tentacles_manager.api.ensure_tentacle_info()\n    classes = [\n        tentacle_class.__name__\n        for tentacle_class in tentacles_configuration.get_all_exchange_tentacles()\n        if not (tentacle_class.is_default_exchange() or tentacle_class.__name__ == exchanges.ExchangeSimulator.__name__)\n    ]\n    if profile_data:\n        try:\n            classes.extend(\n                # always use tentacle class names here as tentacles are indexed by name\n                tentacle_data.name if extra_tentacle_names and tentacle_data.name in extra_tentacle_names\n                else octobot_tentacles_manager.api.get_tentacle_class_from_string(tentacle_data.name).__name__\n                for tentacle_data in profile_data.tentacles\n            )\n        except RuntimeError as err:\n            raise scr_errors.InvalidTentacleProfileError(err) from err\n        if extra_tentacle_names:\n            classes.extend(extra_tentacle_names)\n    return octobot_tentacles_manager.api.create_tentacles_setup_config_with_tentacles(*classes)\n\n\ndef merge_profile_data(\n    profile_data: commons_profiles.ProfileData,\n    previous_profile_data: commons_profiles.ProfileData,\n) -> commons_profiles.ProfileData:\n    # previous config crypto currencies are merged\n    current_traded_pairs = set(get_traded_symbols(profile_data))\n    for currency_data in previous_profile_data.crypto_currencies:\n        for previous_traded_pair in currency_data.trading_pairs:\n            to_add_pairs = set()\n            if previous_traded_pair not in current_traded_pairs:\n                # add pair\n                to_add_pairs.add(previous_traded_pair)\n            parsed_symbol = octobot_commons.symbols.parse_symbol(previous_traded_pair)\n            if parsed_symbol.quote != profile_data.trading.reference_market:\n                # reference market changed: also include the base of this pair within the traded pairs\n                ref_market_pair = octobot_commons.symbols.merge_currencies(\n                    parsed_symbol.base, profile_data.trading.reference_market\n                )\n                if ref_market_pair not in current_traded_pairs:\n                    to_add_pairs.add(ref_market_pair)\n            for traded_pair in to_add_pairs:\n                _get_logger().info(\n                    f\"Profile data merge: including previous config {currency_data} currency into current profile data\"\n                )\n                expand_traded_pairs_into_currencies(profile_data, [traded_pair])\n                current_traded_pairs.add(traded_pair)\n    return profile_data\n\n\n\ndef apply_leverage_config(profile_data: commons_profiles.ProfileData):\n    if leverage := profile_data.future_exchange_data.default_leverage:\n        trading_mode_config = _get_trading_mode_config(profile_data)\n        apply_leverage_config_to_trading_mode_config_if_necessary(trading_mode_config, leverage)\n\n\ndef apply_leverage_config_to_trading_mode_config_if_necessary(trading_mode_config: dict, leverage: float):\n    if trading_constants.CONFIG_LEVERAGE not in trading_mode_config:\n        trading_mode_config[trading_constants.CONFIG_LEVERAGE] = leverage\n\ndef _get_trading_mode_config(profile_data: commons_profiles.ProfileData):\n    trading_mode = get_trading_mode(profile_data)\n    config_by_tentacle = get_config_by_tentacle(profile_data)\n    if trading_mode in config_by_tentacle:\n        return config_by_tentacle[trading_mode]\n    raise KeyError(f\"No trading mode config found in {list(config_by_tentacle)} tentacles config\")\n\n\ndef get_trading_mode(profile_data: commons_profiles.ProfileData) -> typing.Optional[str]:\n    for tentacle_name in get_config_by_tentacle(profile_data):\n        if tentacles_configuration.is_trading_mode_tentacle(tentacle_name):\n            return tentacle_name\n    return None\n\n\ndef get_traded_symbols(\n    profile_data: commons_profiles.ProfileData\n) -> list[str]:\n    symbols = []\n    for crypto_currency in profile_data.crypto_currencies:\n        symbols.extend(crypto_currency.trading_pairs)\n    return symbols\n\n\ndef get_traded_coins(\n    profile_data: commons_profiles.ProfileData,\n    include_stablecoins: bool,\n) -> list[str]:\n    # return an ordered list of:\n    # 1. reference market\n    # 2. traded assets\n    # 3. stablecoins if include_stablecoins is True\n    coins = [profile_data.trading.reference_market, ]\n    for symbol in get_traded_symbols(profile_data):\n        base, quote = octobot_commons.symbols.parse_symbol(symbol).base_and_quote()\n        if base not in coins:\n            coins.append(base)\n        if quote not in coins:\n            coins.append(quote)\n    if include_stablecoins:\n        coins.extend(tuple(\n            coin\n            for coin in common_constants.USD_LIKE_AND_FIAT_COINS\n            if coin not in coins\n        ))\n    return coins\n\n\ndef get_time_frames(\n    profile_data: commons_profiles.ProfileData, for_historical_data=False\n):\n    for config in get_config_by_tentacle(profile_data).values():\n        if evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME in config:\n            return config[evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME]\n    return [_get_default_time_frame(profile_data, for_historical_data)]\n\n\ndef _get_default_time_frame(profile_data: commons_profiles.ProfileData, for_historical_data: bool):\n    if not for_historical_data:\n        # always use DEFAULT_TIMEFRAME when focusing on historical data\n        return scr_constants.DEFAULT_TIMEFRAME.value\n    return _get_historical_default_time_frame(profile_data)\n\n\ndef _get_historical_default_time_frame(profile_data: commons_profiles.ProfileData):\n    if time_frame := get_default_historical_time_frame(profile_data):\n        return time_frame.value\n    # fallback to default timeframe\n    return scr_constants.DEFAULT_TIMEFRAME.value\n\n\ndef requires_price_update_timeframe(profile_data: commons_profiles.ProfileData) -> bool:\n    if trading_mode := get_trading_mode(profile_data):\n        return octobot_tentacles_manager.api.get_tentacle_class_from_string(\n            trading_mode\n        ).use_backtesting_accurate_price_update()\n    return True\n\n\ndef get_default_historical_time_frame(profile_data: commons_profiles.ProfileData) -> typing.Optional[common_enums.TimeFrames]:\n    if trading_mode := get_trading_mode(profile_data):\n        return octobot_tentacles_manager.api.get_tentacle_class_from_string(\n            trading_mode\n        ).get_default_historical_time_frame()\n    return None\n\n\ndef can_convert_ref_market_to_usd_like(\n    exchange_data: exchange_data_import.ExchangeData,\n    profile_data: commons_profiles.ProfileData\n):\n    return can_convert_ref_market_to_usd_like_from_symbols(\n        profile_data.trading.reference_market,\n        [market.symbol for market in exchange_data.markets]\n    )\n\ndef can_convert_ref_market_to_usd_like_from_symbols(\n    reference_market: str,\n    symbols: list[str]\n):\n    if octobot_trading.api.is_usd_like_coin(reference_market):\n        return True\n    for symbol in symbols:\n        if (\n            reference_market in octobot_commons.symbols.parse_symbol(symbol).base_and_quote()\n            and octobot_trading.api.can_convert_symbol_to_usd_like(symbol)\n        ):\n            return True\n    return False\n\n\ndef set_backtesting_portfolio(profile_data, exchange_data):\n    exchange_data.portfolio_details.content = {\n        asset: {\n            common_constants.PORTFOLIO_AVAILABLE: value,\n            common_constants.PORTFOLIO_TOTAL: value\n        }\n        for asset, value in profile_data.backtesting_context.starting_portfolio.items()\n    }\n    _get_logger().info(\n        f\"Applied {profile_data.profile_details.name} backtesting starting \"\n        f\"portfolio: {profile_data.backtesting_context.starting_portfolio}\"\n    )\n\n\ndef get_oldest_historical_config_symbols_and_time(profile_data: commons_profiles.ProfileData, default) -> (list, float):\n    first_historical_config_time = _get_first_historical_config_time(profile_data, default)\n    if first_historical_config_time == default:\n        base_traded_symbols = get_traded_symbols(profile_data)\n        return base_traded_symbols, base_traded_symbols, default\n    first_traded_symbols = _get_all_tentacles_configured_traded_symbols(profile_data, first_historical_config_time)\n    last_traded_symbols = _get_all_tentacles_configured_traded_symbols(profile_data, None)\n    return list(first_traded_symbols), list(last_traded_symbols), first_historical_config_time\n\n\ndef _get_all_tentacles_configured_traded_symbols(\n    profile_data: commons_profiles.ProfileData, first_historical_config_time: typing.Optional[float]\n) -> set:\n    traded_symbols = set()\n    tentacles_config = get_config_by_tentacle(profile_data)\n    for tentacle, tentacle_config in tentacles_config.items():\n        if first_historical_config_time is None:\n            config = tentacle_config\n        else:\n            try:\n                config = commons_configuration.get_historical_tentacle_config(\n                    tentacle_config, first_historical_config_time\n                )\n            except KeyError as err:\n                if tentacles_configuration.is_exchange_tentacle(tentacle):\n                    # exchange tentacles (like HollaEx exchanges) don't have historical configuration: this is normal\n                    pass\n                else:\n                    raise scr_errors.InvalidProfileError(f\"{tentacle} tentacle config is invalid: {err}\")\n        traded_symbols.update(get_tentacle_config_traded_symbols(\n            tentacle, config, profile_data.trading.reference_market\n        ))\n    return traded_symbols\n\n\ndef _get_first_historical_config_time(profile_data: commons_profiles.ProfileData, default) -> float:\n    tentacles_config = get_config_by_tentacle(profile_data)\n    oldest_config_times = []\n    for tentacle, config in tentacles_config.items():\n        try:\n            oldest_config_times.append(\n                commons_configuration.get_oldest_historical_tentacle_config_time(\n                    config\n                )\n            )\n        except ValueError:\n            # no historical config\n            pass\n    if oldest_config_times:\n        # return the most recent of the oldest configurations\n        return max(oldest_config_times)\n    return default\n\n\ndef get_tentacle_config_traded_symbols(tentacle: str, config: dict, reference_market: str) -> set:\n    tentacle_class = octobot_tentacles_manager.api.get_tentacle_class_from_string(tentacle)\n    try:\n        return set(tentacle_class.get_tentacle_config_traded_symbols(config, reference_market))\n    except NotImplementedError as err:\n        if tentacles_configuration.is_exchange_tentacle(tentacle):\n            # exchange tentacles don't implement get_tentacle_config_traded_symbols, this is normal\n            pass\n        else:\n            _get_logger().warning(\n                f\"Trying to get tentacle config historical traded symbols for {tentacle}: {err}\"\n            )\n        return set()\n\n\ndef _get_logger():\n    return octobot_commons.logging.get_logger(\"ScriptedProfileData\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/configuration/tentacles_configuration.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport functools\n\nimport octobot_commons.tentacles_management as tentacles_management\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.modes\n\nimport octobot_tentacles_manager.api\n\n_EXPECTED_MAX_TENTACLES_COUNT = 256\n\n\ndef get_config_history_propagated_tentacles_config_keys(tentacle: str) -> list[str]:\n    tentacle_class = octobot_tentacles_manager.api.get_tentacle_class_from_string(tentacle)\n    return tentacle_class.get_config_history_propagated_tentacles_config_keys()\n\n\n# cached to avoid calling default_parents_inspection when unnecessary\n@functools.lru_cache(maxsize=_EXPECTED_MAX_TENTACLES_COUNT)\ndef is_trading_mode_tentacle(tentacle_name: str) -> bool:\n    tentacle_class = octobot_tentacles_manager.api.get_tentacle_class_from_string(tentacle_name)\n    return tentacles_management.default_parents_inspection(tentacle_class, octobot_trading.modes.AbstractTradingMode)\n\n\n# cached to avoid calling default_parents_inspection when unnecessary\n@functools.lru_cache(maxsize=_EXPECTED_MAX_TENTACLES_COUNT)\ndef is_exchange_tentacle(tentacle_name: str) -> bool:\n    tentacle_class = octobot_tentacles_manager.api.get_tentacle_class_from_string(tentacle_name)\n    return tentacles_management.default_parents_inspection(tentacle_class, exchanges.RestExchange)\n\n\n# cached to avoid calling default_parents_inspection when unnecessary\n@functools.lru_cache(maxsize=2)\ndef get_all_exchange_tentacles() -> list[type[exchanges.RestExchange]]:\n    return tentacles_management.get_all_classes_from_parent(exchanges.RestExchange)\n\n\ndef get_exchange_tentacle_from_name(tentacle_name: str) -> type[exchanges.RestExchange]:\n    for exchange_tentacle in get_all_exchange_tentacles():\n        if exchange_tentacle.get_name() == tentacle_name:\n            return exchange_tentacle\n    raise ValueError(f\"No exchange tentacle found for name: {tentacle_name}\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/constants.py",
    "content": "import octobot_commons.enums as common_enums\n\n\nDEFAULT_TIMEFRAME = common_enums.TimeFrames.ONE_HOUR\nPRICE_UPDATE_TIME_FRAME = common_enums.TimeFrames.FIFTEEN_MINUTES\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .reading import *\nfrom .writing import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/reading/__init__.py",
    "content": "from .exchange_public_data import *\nfrom .exchange_private_data import *\nfrom .metadata_reader import *\nfrom .trading_settings import *\n\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/reading/exchange_private_data/__init__.py",
    "content": "from .open_positions import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/reading/exchange_private_data/open_positions.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.constants as commons_constants\nimport octobot_trading.modes.script_keywords as script_keywords\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\n\n\n#todo clear\ndef is_current_contract_inverse(context, symbol=None, side=trading_enums.PositionSide.BOTH.value):\n    return script_keywords.get_position(context, symbol=symbol, side=side).symbol_contract.is_inverse_contract()\n\n\n# returns negative values when in a short position\ndef open_position_size(\n        context,\n        side=trading_enums.PositionSide.BOTH.value,\n        symbol=None,\n        amount_type=commons_constants.PORTFOLIO_TOTAL\n):\n    symbol = symbol or context.symbol\n    if context.exchange_manager.is_future:\n        return script_keywords.get_position(context, symbol, side).size\n    currency = symbol_util.parse_symbol(context.symbol).base\n    portfolio = context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n    return portfolio.get_currency_portfolio(currency).total if amount_type == commons_constants.PORTFOLIO_TOTAL \\\n        else portfolio.get_currency_portfolio(currency).available\n    # todo handle reference market change\n    # todo handle futures: its account balance from exchange\n    # todo handle futures and return negative for shorts\n\n\ndef is_position_open(\n        context,\n        side=None\n):\n    if side is None:\n        long_open = open_position_size(context, side=\"long\") != trading_constants.ZERO\n        short_open = open_position_size(context, side=\"short\") != trading_constants.ZERO\n        return True if long_open or short_open else False\n    else:\n        return open_position_size(context, side=side) != trading_constants.ZERO\n\n\ndef is_position_long(\n        context,\n):\n    return script_keywords.get_position(context).is_long()\n\n\ndef is_position_short(\n        context,\n):\n    return script_keywords.get_position(context).is_short()\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/reading/exchange_public_data.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_trading.api as api\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchange_data\nimport octobot_trading.personal_data as personal_data\nimport octobot_trading.exchange_data as exchange_data\nimport octobot_trading.enums as trading_enums\nimport octobot_backtesting.api as backtesting_api\nfrom octobot_trading.modes.script_keywords.basic_keywords import run_persistence as run_persistence\nfrom tentacles.Evaluator.Util.candles_util import CandlesUtil\n\n\n# real time in live mode\n# lowest available candle time on backtesting\ndef current_live_time(context) -> float:\n    return api.get_exchange_current_time(context.exchange_manager)\n\n\ndef symbol_fees(context, symbol=None) -> dict:\n    return context.exchange_manager.exchange.get_fees(symbol or context.symbol)\n\n\ndef is_futures_trading(context) -> bool:\n    return context.exchange_manager.is_future\n\n\ndef _time_frame_to_sec(context, time_frame=None):\n    return commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(time_frame or context.time_frame)] * \\\n            commons_constants.MINUTE_TO_SECONDS\n\n\nasync def current_candle_time(context, symbol=None, time_frame=None, use_close_time=False):\n    symbol = symbol or context.symbol\n    time_frame = time_frame or context.time_frame\n    candles_manager = api.get_symbol_candles_manager(\n        api.get_symbol_data(context.exchange_manager, symbol, allow_creation=False), time_frame\n    )\n    if use_close_time:\n        return candles_manager.time_candles[candles_manager.time_candles_index - 1] + \\\n               _time_frame_to_sec(context, time_frame)\n    return candles_manager.time_candles[candles_manager.time_candles_index - 1]\n\n\nasync def current_closed_candle_time(context, symbol=None, time_frame=None):\n    return await current_candle_time(context, symbol=symbol, time_frame=time_frame) \\\n        - _time_frame_to_sec(context, time_frame)\n\n\n# Use capital letters to avoid python native lib conflicts\nasync def Time(context, symbol=None, time_frame=None, limit=-1, max_history=False, use_close_time=True):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n    if max_history and isinstance(candles_manager, octobot_trading.exchange_data.PreloadedCandlesManager):\n        time_data = candles_manager.time_candles\n    else:\n        time_data = candles_manager.get_symbol_time_candles(-1 if max_history else limit)\n    if use_close_time:\n        return [value + _time_frame_to_sec(context, time_frame) for value in time_data]\n    return time_data\n\n\n# real time in live mode\n# lowest available candle closes on backtesting\nasync def current_live_price(context, symbol=None):\n    return await personal_data.get_up_to_date_price(context.exchange_manager, symbol or context.symbol,\n                                                    timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT,\n                                                    base_error=\"Can't get the current price:\")\n\n\nasync def current_candle_price(context, symbol=None, time_frame=None):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, False)\n    return candles_manager.get_symbol_close_candles(1)[-1]\n\n\n# Use capital letters to avoid python native lib conflicts\nasync def Open(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n    if isinstance(candles_manager, octobot_trading.exchange_data.PreloadedCandlesManager) and max_history:\n        return candles_manager.open_candles\n    return candles_manager.get_symbol_open_candles(-1 if max_history else limit)\n\n\n# Use capital letters to avoid python native lib conflicts\nasync def High(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n    if isinstance(candles_manager, octobot_trading.exchange_data.PreloadedCandlesManager) and max_history:\n        return candles_manager.high_candles\n    return candles_manager.get_symbol_high_candles(-1 if max_history else limit)\n\n\n# Use capital letters to avoid python native lib conflicts\nasync def Low(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n    if isinstance(candles_manager, octobot_trading.exchange_data.PreloadedCandlesManager) and max_history:\n        return candles_manager.low_candles\n    return candles_manager.get_symbol_low_candles(-1 if max_history else limit)\n\n\n# Use capital letters to avoid python native lib conflicts\nasync def Close(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n    if isinstance(candles_manager, octobot_trading.exchange_data.PreloadedCandlesManager) and max_history:\n        return candles_manager.close_candles\n    return candles_manager.get_symbol_close_candles(-1 if max_history else limit)\n\n\nasync def hl2(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    try:\n        from tentacles.Evaluator.Util.candles_util import CandlesUtil\n        candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n        return CandlesUtil.HL2(\n            candles_manager.get_symbol_high_candles(-1 if max_history else limit),\n            candles_manager.get_symbol_low_candles(-1 if max_history else limit)\n        )\n    except ImportError:\n        raise RuntimeError(\"CandlesUtil tentacle is required to use HL2\")\n\n\nasync def hlc3(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    try:\n        from tentacles.Evaluator.Util.candles_util import CandlesUtil\n        candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n        return CandlesUtil.HLC3(\n            candles_manager.get_symbol_high_candles(-1 if max_history else limit),\n            candles_manager.get_symbol_low_candles(-1 if max_history else limit),\n            candles_manager.get_symbol_close_candles(-1 if max_history else limit)\n        )\n    except ImportError:\n        raise RuntimeError(\"CandlesUtil tentacle is required to use HLC3\")\n\n\nasync def ohlc4(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    try:\n        from tentacles.Evaluator.Util.candles_util import CandlesUtil\n        candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n        return CandlesUtil.OHLC4(\n            candles_manager.get_symbol_open_candles(-1 if max_history else limit),\n            candles_manager.get_symbol_high_candles(-1 if max_history else limit),\n            candles_manager.get_symbol_low_candles(-1 if max_history else limit),\n            candles_manager.get_symbol_close_candles(-1 if max_history else limit)\n        )\n    except ImportError:\n        raise RuntimeError(\"CandlesUtil tentacle is required to use OHLC4\")\n\n\n# Use capital letters to avoid python native lib conflicts\nasync def Volume(context, symbol=None, time_frame=None, limit=-1, max_history=False):\n    candles_manager = await _get_candle_manager(context, symbol, time_frame, max_history)\n    if isinstance(candles_manager, octobot_trading.exchange_data.PreloadedCandlesManager) and max_history:\n        return candles_manager.close_candles\n    return candles_manager.get_symbol_volume_candles(-1 if max_history else limit)\n\n\nasync def get_candles_from_name(ctx, source_name=\"low\", time_frame=None, symbol=None, limit=-1, max_history=False):\n    \"\"\"\n    source_name can be:\n    \"open\", \"high\", \"low\", \"close\", \"hl2\", \"hlc3\", \"ohlc4\", \"volume\",\n    \"Heikin Ashi close\", \"Heikin Ashi open\", \"Heikin Ashi high\", \"Heikin Ashi low\"\n    \"\"\"\n    symbol = symbol or ctx.symbol\n    time_frame = time_frame or ctx.time_frame\n    if source_name == \"close\":\n        return await Close(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"open\":\n        return await Open(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"high\":\n        return await High(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"low\":\n        return await Low(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"volume\":\n        return await Volume(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"time\":\n        return await Time(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"hl2\":\n        return await hl2(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"hlc3\":\n        return await hlc3(ctx, symbol, time_frame, limit, max_history)\n    if source_name == \"ohlc4\":\n        return await ohlc4(ctx, symbol, time_frame, limit, max_history)\n    if \"Heikin Ashi\" in source_name:\n        haOpen, haHigh, haLow, haClose = CandlesUtil.HeikinAshi(await Open(ctx, symbol, time_frame, limit, max_history),\n                                                                await High(ctx, symbol, time_frame, limit, max_history),\n                                                                await Low(ctx, symbol, time_frame, limit, max_history),\n                                                                await Close(ctx, symbol, time_frame, limit, max_history)\n                                                                )\n        if source_name == \"Heikin Ashi close\":\n            return haClose\n        if source_name == \"Heikin Ashi open\":\n            return haOpen\n        if source_name == \"Heikin Ashi high\":\n            return haHigh\n        if source_name == \"Heikin Ashi low\":\n            return haLow\n\n\nasync def _local_candles_manager(exchange_manager, symbol, time_frame, start_timestamp, end_timestamp):\n    # warning: should only be called with an exchange simulator (in backtesting)\n    ohlcv_data: list = await exchange_manager.exchange.exchange_importers[0].get_ohlcv(\n        exchange_name=exchange_manager.exchange_name,\n        symbol=symbol,\n        time_frame=commons_enums.TimeFrames(time_frame))\n    chronological_candles = sorted(ohlcv_data, key=lambda candle: candle[0])\n    full_candles_history = [\n        ohlcv[-1]\n        for ohlcv in chronological_candles\n        if start_timestamp <= ohlcv[0] <= end_timestamp\n    ]\n    candles_manager = exchange_data.CandlesManager(max_candles_count=len(full_candles_history))\n    await candles_manager.initialize()\n    candles_manager.replace_all_candles(full_candles_history)\n    return candles_manager\n\n\nasync def _get_candle_manager(context, symbol, time_frame, max_history):\n    symbol = symbol or context.symbol\n    time_frame = time_frame or context.time_frame\n    candle_manager = api.get_symbol_candles_manager(\n        api.get_symbol_data(context.exchange_manager, symbol, allow_creation=False), time_frame\n    )\n    if max_history and context.exchange_manager.is_backtesting:\n        if isinstance(candle_manager, octobot_trading.exchange_data.PreloadedCandlesManager):\n            return candle_manager\n        start_timestamp = backtesting_api.get_backtesting_starting_time(context.exchange_manager.exchange.backtesting)\n        end_timestamp = backtesting_api.get_backtesting_ending_time(context.exchange_manager.exchange.backtesting)\n        _key = symbol + time_frame + str(start_timestamp) + str(end_timestamp)\n        try:\n            return run_persistence.get_shared_element(_key)\n        except KeyError:\n            run_persistence.set_shared_element(\n                _key,\n                await _local_candles_manager(\n                    context.exchange_manager, symbol, time_frame, start_timestamp, end_timestamp\n                )\n            )\n            return run_persistence.get_shared_element(_key)\n    return candle_manager\n\n\ndef get_digits_adapted_price(context, price, truncate=True):\n    symbol_market = context.exchange_manager.exchange.get_market_status(context.symbol, with_fixer=False)\n    return personal_data.decimal_adapt_price(symbol_market, price, truncate=truncate)\n\n\ndef get_digits_adapted_amount(context, amount, truncate=True):\n    symbol_market = context.exchange_manager.exchange.get_market_status(context.symbol, with_fixer=False)\n    return personal_data.decimal_adapt_quantity(symbol_market, amount, truncate=truncate)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/reading/metadata_reader.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.databases as databases\n\n\nclass MetadataReader(databases.DBReader):\n    async def read(self) -> list:\n        return await self.all(commons_enums.DBTables.METADATA.value)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/reading/trading_settings.py",
    "content": "def set_initialized_evaluation(ctx, trading_mode, initialized=True, symbol=None, time_frame=None):\n    trading_mode.set_initialized_trading_pair_by_bot_id(symbol or ctx.symbol, time_frame or ctx.time_frame, initialized)\n\n\ndef get_initialized_evaluation(ctx, trading_mode, symbol=None, time_frame=None):\n    return trading_mode.get_initialized_trading_pair_by_bot_id(symbol or ctx.symbol, time_frame or ctx.time_frame)\n\n\ndef are_all_evaluation_initialized(ctx, trading_mode):\n    for symbol in ctx.exchange_manager.exchange_config.traded_symbol_pairs:\n        for time_frame in ctx.exchange_manager.exchange_config.get_relevant_time_frames():\n            try:\n                if not get_initialized_evaluation(ctx, trading_mode, symbol=symbol, time_frame=time_frame.value):\n                    return False\n            except KeyError:\n                return False\n    return True\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/writing/__init__.py",
    "content": "from .plotting import *\nfrom .portfolio import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/writing/plotting.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport numpy\n\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_public_data as exchange_public_data\nimport octobot_trading.modes.script_keywords as script_keywords\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\n\n\nasync def disable_candles_plot(ctx, time_frame=None):\n    time_frame = time_frame or ctx.time_frame\n    if not ctx.symbol_writer.are_data_initialized_by_key.get(time_frame):\n        await script_keywords.disable_candles_plot(None, ctx.exchange_manager)\n\n\nasync def plot(ctx, title, x=None,\n               y=None, z=None, open=None, high=None, low=None, close=None, volume=None,\n               text=None, kind=\"scattergl\", mode=\"lines\", line_shape=\"linear\",\n               condition=None, x_function=exchange_public_data.Time,\n               x_multiplier=1000, time_frame=None,\n               chart=commons_enums.PlotCharts.SUB_CHART.value,\n               cache_value=None, own_yaxis=False, color=None, size=None, shape=None,\n               shift_to_open_candle_time=True):\n    time_frame = time_frame or ctx.time_frame\n    if condition is not None and cache_value is None:\n        if isinstance(ctx.symbol_writer.get_serializable_value(condition), bool):\n            if condition:\n                x = numpy.array(((await x_function(ctx, ctx.symbol, time_frame))[-1],))\n                y = numpy.array((y[-1],))\n            else:\n                x = []\n                y = []\n        else:\n            candidate_y = []\n            candidate_x = []\n            x_data = (await x_function(ctx, ctx.symbol, time_frame))[-len(condition):]\n            y_data = y[-len(condition):]\n            for index, value in enumerate(condition):\n                if value:\n                    candidate_y.append(y_data[index])\n                    candidate_x.append(x_data[index])\n            x = numpy.array(candidate_x)\n            y = numpy.array(candidate_y)\n    count_query = {\n        \"time_frame\": ctx.time_frame,\n    }\n    cache_full_path = None\n    if cache_value is not None:\n        cache_full_path = ctx.get_cache_path(ctx.tentacle)\n        count_query[\"title\"] = title\n        count_query[\"value\"] = cache_full_path\n\n    x_shift = -commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(ctx.time_frame)] * \\\n        commons_constants.MINUTE_TO_SECONDS if shift_to_open_candle_time else 0\n    if not await ctx.symbol_writer.contains_row(\n            commons_enums.DBTables.CACHE_SOURCE.value if cache_value is not None else title,\n            count_query\n    ):\n        if cache_value is not None:\n            table = commons_enums.DBTables.CACHE_SOURCE.value\n            # save x_shift to be applied when displaying and not to change actual cached values\n            cache_data = {\n                \"title\": title,\n                \"text\": text,\n                \"time_frame\": ctx.time_frame,\n                \"value\": cache_full_path,\n                \"cache_value\": cache_value,\n                \"kind\": kind,\n                \"mode\": mode,\n                \"line_shape\": line_shape,\n                \"chart\": chart,\n                \"own_yaxis\": own_yaxis,\n                \"condition\": condition,\n                \"color\": color,\n                \"size\": size,\n                \"shape\": shape,\n                \"x_shift\": x_shift,\n            }\n            update_query = await ctx.symbol_writer.search()\n            update_query = ((update_query.kind == kind)\n                            & (update_query.mode == mode)\n                            & (update_query.time_frame == ctx.time_frame)\n                            & (update_query.title == title))\n            await ctx.symbol_writer.upsert(table, cache_data, update_query)\n        else:\n            adapted_x = None\n            if x is not None:\n                try:\n                    min_available_data = len(x)\n                except TypeError:\n                    min_available_data = None\n                if y is not None:\n                    min_available_data = len(y)\n                    if isinstance(y, list) and not isinstance(x, list):\n                        x = [x] * len(y)\n                if z is not None:\n                    min_available_data = len(z) if min_available_data is None else min(min_available_data, len(z))\n                    if isinstance(z, list) and not isinstance(z, list):\n                        x = [x] * len(z)\n                adapted_x = x[-min_available_data:] if min_available_data != len(x) else x\n            if adapted_x is None:\n                raise RuntimeError(\"No confirmed adapted_x\")\n            adapted_x = [(a_x + x_shift) * x_multiplier for a_x in adapted_x] if isinstance(adapted_x, list) \\\n                else adapted_x * x_multiplier\n            await ctx.symbol_writer.log_many(\n                title,\n                [\n                    {\n                        \"x\": value,\n                        \"y\": _get_value_from_array(y, index),\n                        \"z\": _get_value_from_array(z, index),\n                        \"open\": _get_value_from_array(open, index),\n                        \"high\": _get_value_from_array(high, index),\n                        \"low\": _get_value_from_array(low, index),\n                        \"close\": _get_value_from_array(close, index),\n                        \"volume\": _get_value_from_array(volume, index),\n                        \"time_frame\": ctx.time_frame,\n                        \"kind\": kind,\n                        \"mode\": mode,\n                        \"line_shape\": line_shape,\n                        \"chart\": chart,\n                        \"own_yaxis\": own_yaxis,\n                        \"color\": color,\n                        \"text\": text,\n                        \"size\": size,\n                        \"shape\": shape,\n                    }\n                    for index, value in enumerate(adapted_x)\n                ],\n                cache=False\n            )\n    elif cache_value is None and x is not None:\n        if isinstance(y, list) and not isinstance(x, list):\n            x = [x] * len(y)\n        elif isinstance(z, list) and not isinstance(x, list):\n            x = [x] * len(z)\n        if len(x) and \\\n                not await ctx.symbol_writer.contains_row(title,\n                                                   {\"x\": _get_value_from_array(x, -1) * x_multiplier}):\n            x_value = (_get_value_from_array(x, -1) + x_shift) * x_multiplier\n            await ctx.symbol_writer.upsert(\n                title,\n                {\n                    \"time_frame\": ctx.time_frame,\n                    \"x\": x_value,\n                    \"y\": _get_value_from_array(y, -1),\n                    \"z\": _get_value_from_array(z, -1),\n                    \"open\": _get_value_from_array(open, -1),\n                    \"high\": _get_value_from_array(high, -1),\n                    \"low\": _get_value_from_array(low, -1),\n                    \"close\": _get_value_from_array(close, -1),\n                    \"volume\": _get_value_from_array(volume, -1),\n                    \"kind\": kind,\n                    \"mode\": mode,\n                    \"line_shape\": line_shape,\n                    \"chart\": chart,\n                    \"own_yaxis\": own_yaxis,\n                    \"color\": color,\n                    \"text\": text,\n                    \"size\": size,\n                    \"shape\": shape,\n                },\n                None,\n                cache_query={\"x\": x_value}\n            )\n\n\nasync def plot_shape(ctx, title, value, y_value,\n                     chart=commons_enums.PlotCharts.SUB_CHART.value,\n                     kind=\"scattergl\", mode=\"markers\", line_shape=\"linear\", x_multiplier=1000):\n    if not await ctx.symbol_writer.contains_row(title, {\n        \"x\": ctx.x,\n        \"time_frame\": ctx.time_frame\n    }):\n        await ctx.symbol_writer.log(\n            title,\n            {\n                \"time_frame\": ctx.time_frame,\n                \"x\": (await exchange_public_data.current_candle_time(ctx)) * x_multiplier,\n                \"y\": y_value,\n                \"value\": ctx.symbol_writer.get_serializable_value(value),\n                \"kind\": kind,\n                \"mode\": mode,\n                \"line_shape\": line_shape,\n                \"chart\": chart,\n            }\n        )\n\n\ndef _get_value_from_array(array, index, multiplier=1):\n    if array is None:\n        return None\n    return array[index] * multiplier\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/data/writing/portfolio.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_private_data as exchange_private_data\n\n\nasync def withdraw(context, amount, currency):\n    if not context.exchange_manager.is_backtesting:\n        raise RuntimeError(\"withdraw is only supported in backtesting\")\n    amount_type, amount_value = script_keywords.parse_quantity(amount)\n\n    if amount_type is script_keywords.QuantityType.UNKNOWN or amount_value <= 0:\n        raise trading_errors.InvalidArgumentError(\"amount cant be zero or negative\")\n    if amount_type in (script_keywords.QuantityType.DELTA, script_keywords.QuantityType.DELTA_BASE):\n        # nothing to do\n        pass\n    elif amount_type is script_keywords.QuantityType.PERCENT:\n        amount_value = script_keywords.account_holdings(context, currency) * amount_value / 100\n    else:\n        raise trading_errors.InvalidArgumentError(\"make sure to use a supported syntax for amount\")\n    await context.trader.withdraw(amount_value, currency)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/errors.py",
    "content": "class ScriptedLibraryError(Exception):\n    pass\n\n\nclass InvalidBacktestingDataError(ScriptedLibraryError):\n    pass\n\n\nclass MissingReadOnlyExchangeCredentialsError(ScriptedLibraryError):\n    pass\n\n\nclass InvalidProfileError(ScriptedLibraryError):\n    pass\n\n\nclass InvalidTentacleProfileError(InvalidProfileError):\n    pass\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/exchanges/__init__.py",
    "content": "from tentacles.Meta.Keywords.scripting_library.exchanges.local_exchange import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/exchanges/local_exchange.py",
    "content": "import contextlib\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.exchange_data as exchange_data_import\n\nimport tentacles.Meta.Keywords.scripting_library.configuration.profile_data_configuration as profile_data_configuration\n\n\n@contextlib.asynccontextmanager\nasync def local_ccxt_exchange_manager(\n    exchange_data: exchange_data_import.ExchangeData,\n    tentacles_setup_config,\n    exchange_config_by_exchange: typing.Optional[dict[str, dict]] = None,\n):\n    exchange_config = profile_data_configuration.get_exchange_config(\n        exchange_data, tentacles_setup_config, exchange_config_by_exchange, False\n    )\n    ignore_config = not profile_data_configuration.is_auth_required_exchanges(\n        exchange_data, tentacles_setup_config, exchange_config_by_exchange\n    )\n    async with exchanges.get_local_exchange_manager(\n        exchange_data.exchange_details.name, exchange_config, tentacles_setup_config,\n        exchange_data.auth_details.sandboxed, ignore_config=ignore_config,\n        use_cached_markets=True,\n        is_broker_enabled=exchange_data.auth_details.broker_enabled,\n        exchange_config_by_exchange=exchange_config_by_exchange,\n        disable_unauth_retry=True,  # unauth fallback is never required here, if auth fails, this should fail\n    ) as exchange_manager:\n        yield exchange_manager\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom .order_types import *\nfrom .position_size import *\nfrom .order_tags import *\nfrom .grouping import *\nfrom .cancelling import *\nfrom .editing import *\nfrom .chaining import *\nfrom .open_orders import *\nfrom .waiting import *\nfrom .mocks import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/cancelling.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.enums as enums\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\nimport tentacles.Meta.Keywords.scripting_library.orders.order_tags as order_tags\n\n\nasync def cancel_orders(\n    ctx, which=\"all\", symbol=None, symbols=None,\n    cancel_loaded_orders=True, since: int or float = -1,\n    until: int or float = -1,\n) -> bool:\n    symbols = symbols or [symbol] if symbol or symbols else [ctx.symbol]\n    orders = None\n    orders_canceled = False\n    side = None\n    if which == \"all\":\n        side = None\n    elif which == \"sell\":\n        side = enums.TradeOrderSide.SELL\n    elif which == \"buy\":\n        side = enums.TradeOrderSide.BUY\n    else:  # tagged order\n        orders = order_tags.get_tagged_orders(\n            ctx, which, symbol=symbol, since=since, until=until)\n    if orders is not None:\n        for order in orders:\n            if await ctx.trader.cancel_order(order):\n                orders_canceled = True\n                if basic_keywords.is_emitting_trading_signals(ctx):\n                    ctx.get_signal_builder().add_cancelled_order(order, ctx.trader.exchange_manager)\n    else:\n        for symbol in symbols:\n            orders_canceled, orders = await ctx.trader.cancel_open_orders(\n                symbol, cancel_loaded_orders=cancel_loaded_orders,\n                side=side, since=since, until=until)\n            if basic_keywords.is_emitting_trading_signals(ctx):\n                for order in orders:\n                    ctx.get_signal_builder().add_cancelled_order(order, ctx.trader.exchange_manager)\n    return orders_canceled\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/chaining.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.personal_data as personal_data\n\n\nasync def chain_order(base_order, chained_orders, update_with_triggering_order_fees=False) -> list:\n    # order creation return a list by default, handle it here\n    orders = []\n    if isinstance(base_order, list):\n        if not base_order:\n            return orders\n        base_order = base_order[0]\n    if not isinstance(chained_orders, list):\n        chained_orders = [chained_orders]\n    for order in chained_orders:\n        await order.set_as_chained_order(base_order, False, {}, update_with_triggering_order_fees)\n        base_order.add_chained_order(order)\n        if base_order.is_filled() and order.should_be_created():\n            await personal_data.create_as_chained_order(order)\n        orders.append(order)\n    return orders\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/editing.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\n\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\n\n\nasync def edit_order(ctx, order,\n                     edited_quantity: decimal.Decimal = None,\n                     edited_price: decimal.Decimal = None,\n                     edited_stop_price: decimal.Decimal = None,\n                     edited_current_price: decimal.Decimal = None,\n                     params: dict = None) -> bool:\n    if not ctx.enable_trading:\n        return False\n    changed = await ctx.trader.edit_order(\n        order,\n        edited_quantity=edited_quantity,\n        edited_price=edited_price,\n        edited_stop_price=edited_stop_price,\n        edited_current_price=edited_current_price,\n        params=params,\n    )\n    if basic_keywords.is_emitting_trading_signals(ctx):\n        ctx.get_signal_builder().add_edited_order(\n            order,\n            ctx.trader.exchange_manager,\n            updated_quantity=trading_constants.ZERO if edited_quantity is None else edited_quantity,\n            updated_limit_price=trading_constants.ZERO if edited_price is None else edited_price,\n            updated_stop_price=trading_constants.ZERO if edited_stop_price is None else edited_stop_price,\n            updated_current_price=trading_constants.ZERO if edited_current_price is None else edited_current_price\n        )\n    return changed\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/grouping.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\n\n\ndef create_one_cancels_the_other_group(context, group_identifier=None, orders=None) \\\n        -> trading_personal_data.OneCancelsTheOtherOrderGroup:\n    \"\"\"\n    Should be used to create temporary groups binding localized orders, where this group can be\n    created once and directly associated to each order\n    \"\"\"\n    return _create_order_group(context, trading_personal_data.OneCancelsTheOtherOrderGroup, group_identifier, orders)\n\n\ndef get_or_create_one_cancels_the_other_group(\n        context, orders=None, include_chained_orders=True,\n        group_identifier=None) -> trading_personal_data.OneCancelsTheOtherOrderGroup:\n    \"\"\"\n    Should be used to manage long lasting groups that are meant to be re-used\n    First: looks for groups in orders\n    Second: looks for groups named as group_identifier\n    Third: creates a group named as group_identifier\n    \"\"\"\n    if group := get_group_from_orders(orders, include_chained_orders=include_chained_orders):\n        return group\n    return _get_or_create_order_group(context, trading_personal_data.OneCancelsTheOtherOrderGroup, group_identifier)\n\n\ndef create_balanced_take_profit_and_stop_group(context, group_identifier=None, orders=None) \\\n        -> trading_personal_data.BalancedTakeProfitAndStopOrderGroup:\n    \"\"\"\n    Should be used to create temporary groups binding localized orders, where this group can be\n    created once and directly associated to each order\n    \"\"\"\n    return _create_order_group(context, trading_personal_data.BalancedTakeProfitAndStopOrderGroup,\n                               group_identifier, orders)\n\n\ndef get_or_create_balanced_take_profit_and_stop_group(\n        context, orders=None, include_chained_orders=True,\n        group_identifier=None) -> trading_personal_data.BalancedTakeProfitAndStopOrderGroup:\n    \"\"\"\n    Should be used to manage long lasting groups that are meant to be re-used\n    First: looks for groups in orders\n    Second: looks for groups named as group_identifier\n    Third: creates a group named as group_identifier\n    \"\"\"\n    if group := get_group_from_orders(orders, include_chained_orders=include_chained_orders):\n        return group\n    return _get_or_create_order_group(context, trading_personal_data.BalancedTakeProfitAndStopOrderGroup,\n                                      group_identifier)\n\n\ndef add_orders_to_group(ctx, order_group, orders):\n    orders = orders if isinstance(orders, list) else [orders]\n    for order in orders:\n        order.add_to_order_group(order_group)\n        if basic_keywords.is_emitting_trading_signals(ctx):\n            ctx.get_signal_builder().add_order_to_group(order, ctx.exchange_manager)\n\n\ndef get_group_from_orders(orders, include_chained_orders=True):\n    if orders is None:\n        return None\n    orders = orders if isinstance(orders, list) else [orders]\n    for order in orders:\n        if order.order_group is not None:\n            return order.order_group\n        if include_chained_orders:\n            if group := get_group_from_orders(order.chained_orders):\n                return group\n    return None\n\n\ndef get_open_orders_from_group(order_group):\n    return order_group.get_group_open_orders()\n\n\nasync def enable_group(order_group, enabled):\n    await order_group.enable(enabled)\n\n\ndef _create_order_group(context, group_type, group_identifier, orders) -> trading_personal_data.OrderGroup:\n    group = context.exchange_manager.exchange_personal_data.orders_manager.create_group(group_type, group_identifier)\n    if orders is not None:\n        add_orders_to_group(context, group, orders)\n    return group\n\n\ndef _get_or_create_order_group(context, group_type, group_identifier) -> trading_personal_data.OrderGroup:\n    return context.exchange_manager.exchange_personal_data.orders_manager.get_or_create_group(group_type,\n                                                                                              group_identifier)\n\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/mocks.py",
    "content": "import decimal\n\nimport octobot_trading.personal_data as personal_data\n\n\ndef minimal_order_amount(symbol):\n    return BYBIT_SYMBOLS_LIMIT_MIN_AMOUNT_EXTRACT[symbol]\n\n\ndef max_digits(symbol):\n    return BYBIT_SYMBOLS_AMOUNT_MAX_DIGITS_EXTRACT[symbol]\n\n\ndef adapt_digits(symbol, value):\n    if value is not None:\n        return personal_data.decimal_trunc_with_n_decimal_digits(\n            decimal.Decimal(str(value)),\n            decimal.Decimal(str(max_digits(symbol))),\n            truncate=True\n        )\n    return value\n\n\n# todo remove when symbol market status in backtesting data files\n# extract from nov 2 2022\nBYBIT_SYMBOLS_LIMIT_MIN_AMOUNT_EXTRACT = {\n    \"1INCH/USDT:USDT\": 0.1, \"AAVE/USDT:USDT\": 0.01, \"ACH/USDT:USDT\": 10.0, \"ADA/USD:ADA\": 1.0, \"ADA/USDT:USDT\": 1.0,\n    \"AGLD/USDT:USDT\": 0.1, \"AKRO/USDT:USDT\": 100.0, \"ALGO/USDT:USDT\": 0.1, \"ALICE/USDT:USDT\": 0.1,\n    \"ALPHA/USDT:USDT\": 1.0,\n    \"ANKR/USDT:USDT\": 1.0, \"ANT/USDT:USDT\": 0.1, \"APE/USDT:USDT\": 0.1, \"API3/USDT:USDT\": 0.1, \"APT/USDT:USDT\": 0.01,\n    \"ARPA/USDT:USDT\": 10.0, \"AR/USDT:USDT\": 0.1, \"ASTR/USDT:USDT\": 1.0, \"ATOM/USDT:USDT\": 0.1, \"AUDIO/USDT:USDT\": 0.1,\n    \"AVAX/USDT:USDT\": 0.1, \"AXS/USDT:USDT\": 0.1, \"BAKE/USDT:USDT\": 0.1, \"BAL/USDT:USDT\": 0.01, \"BAND/USDT:USDT\": 0.1,\n    \"BAT/USDT:USDT\": 0.1, \"BCH/USDT:USDT\": 0.01, \"BEL/USDT:USDT\": 1.0, \"BICO/USDT:USDT\": 0.1, \"BIT/USD:BIT\": 1.0,\n    \"BIT/USDT:USDT\": 0.1, \"BLZ/USDT:USDT\": 1.0, \"BNB/USDT:USDT\": 0.01, \"BNX/USDT:USDT\": 0.01, \"BOBA/USDT:USDT\": 0.1,\n    \"BSV/USDT:USDT\": 0.01, \"BSW/USDT:USDT\": 1.0, \"BTC/USD:BTC\": 1.0, \"BTC/USDT:USDT\": 0.001, \"C98/USDT:USDT\": 0.1,\n    \"CEEK/USDT:USDT\": 1.0, \"CELO/USDT:USDT\": 0.1, \"CELR/USDT:USDT\": 1.0, \"CHR/USDT:USDT\": 0.1, \"CHZ/USDT:USDT\": 1.0,\n    \"CKB/USDT:USDT\": 10.0, \"COMP/USDT:USDT\": 0.01, \"COTI/USDT:USDT\": 1.0, \"CREAM/USDT:USDT\": 0.01, \"CRO/USDT:USDT\": 1.0,\n    \"CRV/USDT:USDT\": 0.1, \"CTC/USDT:USDT\": 1.0, \"CTK/USDT:USDT\": 0.1, \"CTSI/USDT:USDT\": 1.0, \"CVC/USDT:USDT\": 1.0,\n    \"CVX/USDT:USDT\": 0.01, \"DAR/USDT:USDT\": 0.1, \"DASH/USDT:USDT\": 0.01, \"DENT/USDT:USDT\": 100.0, \"DGB/USDT:USDT\": 10.0,\n    \"DODO/USDT:USDT\": 1.0, \"DOGE/USDT:USDT\": 1.0, \"DOT/USD:DOT\": 1.0, \"DOT/USDT:USDT\": 0.1, \"DUSK/USDT:USDT\": 1.0,\n    \"DYDX/USDT:USDT\": 0.1, \"EGLD/USDT:USDT\": 0.01, \"ENJ/USDT:USDT\": 0.1, \"ENS/USDT:USDT\": 0.1, \"EOS/USD:EOS\": 1.0,\n    \"EOS/USDT:USDT\": 0.1, \"ETC/USDT:USDT\": 0.1, \"ETH/USD:ETH\": 1.0, \"ETH/USDT:USDT\": 0.01, \"ETHW/USDT:USDT\": 0.01,\n    \"FIL/USDT:USDT\": 0.1, \"FITFI/USDT:USDT\": 1.0, \"FLM/USDT:USDT\": 1.0, \"FLOW/USDT:USDT\": 0.1, \"FTM/USDT:USDT\": 1.0,\n    \"FTT/USDT:USDT\": 0.1, \"FXS/USDT:USDT\": 0.01, \"GALA/USDT:USDT\": 1.0, \"GAL/USDT:USDT\": 0.01, \"GLMR/USDT:USDT\": 0.1,\n    \"GMT/USDT:USDT\": 1.0, \"GMX/USDT:USDT\": 0.01, \"GRT/USDT:USDT\": 0.1, \"GTC/USDT:USDT\": 0.1, \"HBAR/USDT:USDT\": 1.0,\n    \"HNT/USDT:USDT\": 0.01, \"HOT/USDT:USDT\": 100.0, \"ICP/USDT:USDT\": 0.1, \"ICX/USDT:USDT\": 1.0, \"ILV/USDT:USDT\": 0.01,\n    \"IMX/USDT:USDT\": 0.1, \"INJ/USDT:USDT\": 0.1, \"IOST/USDT:USDT\": 1.0, \"IOTA/USDT:USDT\": 0.1, \"IOTX/USDT:USDT\": 1.0,\n    \"JASMY/USDT:USDT\": 1.0, \"JST/USDT:USDT\": 10.0, \"KAVA/USDT:USDT\": 0.1, \"KDA/USDT:USDT\": 0.1, \"KLAY/USDT:USDT\": 0.1,\n    \"KNC/USDT:USDT\": 0.1, \"KSM/USDT:USDT\": 0.01, \"LDO/USDT:USDT\": 0.1, \"LINA/USDT:USDT\": 10.0, \"LINK/USDT:USDT\": 0.1,\n    \"LIT/USDT:USDT\": 0.1, \"LOOKS/USDT:USDT\": 0.1, \"LPT/USDT:USDT\": 0.1, \"LRC/USDT:USDT\": 0.1, \"LTC/USD:LTC\": 1.0,\n    \"LTC/USDT:USDT\": 0.1, \"LUNA2/USDT:USDT\": 0.1, \"MANA/USD:MANA\": 1.0, \"MANA/USDT:USDT\": 0.1, \"MASK/USDT:USDT\": 0.1,\n    \"MATIC/USDT:USDT\": 1.0, \"MINA/USDT:USDT\": 0.1, \"MKR/USDT:USDT\": 0.001, \"MTL/USDT:USDT\": 0.1, \"NEAR/USDT:USDT\": 0.1,\n    \"NEO/USDT:USDT\": 0.01, \"OCEAN/USDT:USDT\": 1.0, \"OGN/USDT:USDT\": 1.0, \"OMG/USDT:USDT\": 0.1, \"ONE/USDT:USDT\": 1.0,\n    \"ONT/USDT:USDT\": 1.0, \"OP/USDT:USDT\": 0.1, \"PAXG/USDT:USDT\": 0.001, \"PEOPLE/USDT:USDT\": 1.0, \"QTUM/USDT:USDT\": 0.1,\n    \"RAY/USDT:USDT\": 0.1, \"REEF/USDT:USDT\": 10.0, \"REN/USDT:USDT\": 0.1, \"REQ/USDT:USDT\": 1.0, \"RNDR/USDT:USDT\": 0.1,\n    \"ROSE/USDT:USDT\": 1.0, \"RSR/USDT:USDT\": 10.0, \"RSS3/USDT:USDT\": 1.0, \"RUNE/USDT:USDT\": 0.1, \"RVN/USDT:USDT\": 1.0,\n    \"SAND/USDT:USDT\": 1.0, \"SCRT/USDT:USDT\": 0.1, \"SC/USDT:USDT\": 10.0, \"SFP/USDT:USDT\": 0.1, \"SKL/USDT:USDT\": 1.0,\n    \"SLP/USDT:USDT\": 10.0, \"SNX/USDT:USDT\": 0.1, \"SOL/USD:SOL\": 1.0, \"SOL/USDT:USDT\": 0.1, \"SPELL/USDT:USDT\": 10.0,\n    \"SRM/USDT:USDT\": 0.1, \"STG/USDT:USDT\": 0.1, \"STMX/USDT:USDT\": 10.0, \"STORJ/USDT:USDT\": 0.1, \"STX/USDT:USDT\": 0.1,\n    \"SUN/USDT:USDT\": 10.0, \"SUSHI/USDT:USDT\": 0.1, \"SXP/USDT:USDT\": 0.1, \"THETA/USDT:USDT\": 0.1, \"TLM/USDT:USDT\": 1.0,\n    \"TOMO/USDT:USDT\": 0.1, \"TRB/USDT:USDT\": 0.01, \"TRX/USDT:USDT\": 1.0, \"UNFI/USDT:USDT\": 0.1, \"UNI/USDT:USDT\": 0.1,\n    \"USDC/USDT:USDT\": 0.1, \"VET/USDT:USDT\": 1.0, \"WAVES/USDT:USDT\": 0.1, \"WOO/USDT:USDT\": 0.1, \"XCN/USDT:USDT\": 10.0,\n    \"XEM/USDT:USDT\": 1.0, \"XLM/USDT:USDT\": 1.0, \"XMR/USDT:USDT\": 0.01, \"XNO/USDT:USDT\": 1.0, \"XRP/USD:XRP\": 1.0,\n    \"XRP/USDT:USDT\": 1.0, \"XTZ/USDT:USDT\": 0.1, \"YFI/USDT:USDT\": 0.0001, \"YGG/USDT:USDT\": 0.1, \"ZEC/USDT:USDT\": 0.01,\n    \"ZEN/USDT:USDT\": 0.1, \"ZIL/USDT:USDT\": 10.0, \"ZRX/USDT:USDT\": 1.0, \"BTC/USD:USDC\": 0.001, \"ETC/USD:USDC\": 0.1,\n    \"MATIC/USD:USDC\": 1.0, \"OP/USD:USDC\": 1.0, \"ETH/USD:USDC\": 0.01, \"GMT/USD:USDC\": 1.0, \"ADA/USD:USDC\": 1.0,\n    \"AVAX/USD:USDC\": 0.01, \"SOL/USD:USDC\": 0.1, \"XRP/USD:USDC\": 1.0, \"SAND/USD:USDC\": 1.0, \"APE/USD:USDC\": 0.1,\n    \"SWEAT/USD:USDC\": 100.0, \"ATOM/USD:USDC\": 0.1, \"EOS/USD:USDC\": 0.1, \"CHZ/USD:USDC\": 1.0, \"NEAR/USD:USDC\": 0.1,\n    \"BNB/USD:USDC\": 0.01, \"LDO/USD:USDC\": 0.1, \"LUNA/USD:USDC\": 0.1, \"APT/USD:USDC\": 0.01}\n\nBYBIT_SYMBOLS_AMOUNT_MAX_DIGITS_EXTRACT = {\n    \"1INCH/USDT:USDT\": 1, \"AAVE/USDT:USDT\": 2, \"ACH/USDT:USDT\": 1,\n    \"ADA/USD:ADA\": 0, \"ADA/USDT:USDT\": 0, \"AGLD/USDT:USDT\": 1,\n    \"AKRO/USDT:USDT\": 2, \"ALGO/USDT:USDT\": 1, \"ALICE/USDT:USDT\": 1,\n    \"ALPHA/USDT:USDT\": 0, \"ANKR/USDT:USDT\": 0, \"ANT/USDT:USDT\": 1,\n    \"APE/USDT:USDT\": 1, \"API3/USDT:USDT\": 1, \"APT/USDT:USDT\": 2,\n    \"ARPA/USDT:USDT\": 1, \"AR/USDT:USDT\": 1, \"ASTR/USDT:USDT\": 0,\n    \"ATOM/USDT:USDT\": 1, \"AUDIO/USDT:USDT\": 1, \"AVAX/USDT:USDT\": 1,\n    \"AXS/USDT:USDT\": 1, \"BAKE/USDT:USDT\": 1, \"BAL/USDT:USDT\": 2,\n    \"BAND/USDT:USDT\": 1, \"BAT/USDT:USDT\": 1, \"BCH/USDT:USDT\": 2,\n    \"BEL/USDT:USDT\": 0, \"BICO/USDT:USDT\": 1, \"BIT/USD:BIT\": 0,\n    \"BIT/USDT:USDT\": 1, \"BLZ/USDT:USDT\": 0, \"BNB/USDT:USDT\": 2,\n    \"BNX/USDT:USDT\": 2, \"BOBA/USDT:USDT\": 1, \"BSV/USDT:USDT\": 2,\n    \"BSW/USDT:USDT\": 0, \"BTC/USD:BTC\": 0, \"BTC/USDT:USDT\": 3, \"C98/USDT:USDT\": 1,\n    \"CEEK/USDT:USDT\": 0, \"CELO/USDT:USDT\": 1, \"CELR/USDT:USDT\": 0,\n    \"CHR/USDT:USDT\": 1, \"CHZ/USDT:USDT\": 0, \"CKB/USDT:USDT\": 1,\n    \"COMP/USDT:USDT\": 2, \"COTI/USDT:USDT\": 0, \"CREAM/USDT:USDT\": 2,\n    \"CRO/USDT:USDT\": 0, \"CRV/USDT:USDT\": 1, \"CTC/USDT:USDT\": 0,\n    \"CTK/USDT:USDT\": 1, \"CTSI/USDT:USDT\": 0, \"CVC/USDT:USDT\": 0,\n    \"CVX/USDT:USDT\": 2, \"DAR/USDT:USDT\": 1, \"DASH/USDT:USDT\": 2,\n    \"DENT/USDT:USDT\": 2, \"DGB/USDT:USDT\": 1, \"DODO/USDT:USDT\": 0,\n    \"DOGE/USDT:USDT\": 0, \"DOT/USD:DOT\": 0, \"DOT/USDT:USDT\": 1,\n    \"DUSK/USDT:USDT\": 0, \"DYDX/USDT:USDT\": 1, \"EGLD/USDT:USDT\": 2,\n    \"ENJ/USDT:USDT\": 1, \"ENS/USDT:USDT\": 1, \"EOS/USD:EOS\": 0, \"EOS/USDT:USDT\": 1,\n    \"ETC/USDT:USDT\": 1, \"ETH/USD:ETH\": 0, \"ETH/USDT:USDT\": 2,\n    \"ETHW/USDT:USDT\": 2, \"FIL/USDT:USDT\": 1, \"FITFI/USDT:USDT\": 0,\n    \"FLM/USDT:USDT\": 0, \"FLOW/USDT:USDT\": 1, \"FTM/USDT:USDT\": 0,\n    \"FTT/USDT:USDT\": 1, \"FXS/USDT:USDT\": 2, \"GALA/USDT:USDT\": 0,\n    \"GAL/USDT:USDT\": 2, \"GLMR/USDT:USDT\": 1, \"GMT/USDT:USDT\": 0,\n    \"GMX/USDT:USDT\": 2, \"GRT/USDT:USDT\": 1, \"GTC/USDT:USDT\": 1,\n    \"HBAR/USDT:USDT\": 0, \"HNT/USDT:USDT\": 2, \"HOT/USDT:USDT\": 2,\n    \"ICP/USDT:USDT\": 1, \"ICX/USDT:USDT\": 0, \"ILV/USDT:USDT\": 2,\n    \"IMX/USDT:USDT\": 1, \"INJ/USDT:USDT\": 1, \"IOST/USDT:USDT\": 0,\n    \"IOTA/USDT:USDT\": 1, \"IOTX/USDT:USDT\": 0, \"JASMY/USDT:USDT\": 0,\n    \"JST/USDT:USDT\": 1, \"KAVA/USDT:USDT\": 1, \"KDA/USDT:USDT\": 1,\n    \"KLAY/USDT:USDT\": 1, \"KNC/USDT:USDT\": 1, \"KSM/USDT:USDT\": 2,\n    \"LDO/USDT:USDT\": 1, \"LINA/USDT:USDT\": 1, \"LINK/USDT:USDT\": 1,\n    \"LIT/USDT:USDT\": 1, \"LOOKS/USDT:USDT\": 1, \"LPT/USDT:USDT\": 1,\n    \"LRC/USDT:USDT\": 1, \"LTC/USD:LTC\": 0, \"LTC/USDT:USDT\": 1,\n    \"LUNA2/USDT:USDT\": 1, \"MANA/USD:MANA\": 0, \"MANA/USDT:USDT\": 1,\n    \"MASK/USDT:USDT\": 1, \"MATIC/USDT:USDT\": 0, \"MINA/USDT:USDT\": 1,\n    \"MKR/USDT:USDT\": 3, \"MTL/USDT:USDT\": 1, \"NEAR/USDT:USDT\": 1,\n    \"NEO/USDT:USDT\": 2, \"OCEAN/USDT:USDT\": 0, \"OGN/USDT:USDT\": 0,\n    \"OMG/USDT:USDT\": 1, \"ONE/USDT:USDT\": 0, \"ONT/USDT:USDT\": 0,\n    \"OP/USDT:USDT\": 1, \"PAXG/USDT:USDT\": 3, \"PEOPLE/USDT:USDT\": 0,\n    \"QTUM/USDT:USDT\": 1, \"RAY/USDT:USDT\": 1, \"REEF/USDT:USDT\": 1,\n    \"REN/USDT:USDT\": 1, \"REQ/USDT:USDT\": 0, \"RNDR/USDT:USDT\": 1,\n    \"ROSE/USDT:USDT\": 0, \"RSR/USDT:USDT\": 1, \"RSS3/USDT:USDT\": 0,\n    \"RUNE/USDT:USDT\": 1, \"RVN/USDT:USDT\": 0, \"SAND/USDT:USDT\": 0,\n    \"SCRT/USDT:USDT\": 1, \"SC/USDT:USDT\": 1, \"SFP/USDT:USDT\": 1,\n    \"SKL/USDT:USDT\": 0, \"SLP/USDT:USDT\": 1, \"SNX/USDT:USDT\": 1, \"SOL/USD:SOL\": 0,\n    \"SOL/USDT:USDT\": 1, \"SPELL/USDT:USDT\": 1, \"SRM/USDT:USDT\": 1,\n    \"STG/USDT:USDT\": 1, \"STMX/USDT:USDT\": 1, \"STORJ/USDT:USDT\": 1,\n    \"STX/USDT:USDT\": 1, \"SUN/USDT:USDT\": 1, \"SUSHI/USDT:USDT\": 1,\n    \"SXP/USDT:USDT\": 1, \"THETA/USDT:USDT\": 1, \"TLM/USDT:USDT\": 0,\n    \"TOMO/USDT:USDT\": 1, \"TRB/USDT:USDT\": 2, \"TRX/USDT:USDT\": 0,\n    \"UNFI/USDT:USDT\": 1, \"UNI/USDT:USDT\": 1, \"USDC/USDT:USDT\": 1,\n    \"VET/USDT:USDT\": 0, \"WAVES/USDT:USDT\": 1, \"WOO/USDT:USDT\": 1,\n    \"XCN/USDT:USDT\": 1, \"XEM/USDT:USDT\": 0, \"XLM/USDT:USDT\": 0,\n    \"XMR/USDT:USDT\": 2, \"XNO/USDT:USDT\": 0, \"XRP/USD:XRP\": 0, \"XRP/USDT:USDT\": 0,\n    \"XTZ/USDT:USDT\": 1, \"YFI/USDT:USDT\": 4, \"YGG/USDT:USDT\": 1,\n    \"ZEC/USDT:USDT\": 2, \"ZEN/USDT:USDT\": 1, \"ZIL/USDT:USDT\": 1,\n    \"ZRX/USDT:USDT\": 0, \"BTC/USD:USDC\": 3, \"ETC/USD:USDC\": 1,\n    \"MATIC/USD:USDC\": 0, \"OP/USD:USDC\": 0, \"ETH/USD:USDC\": 2, \"GMT/USD:USDC\": 0,\n    \"ADA/USD:USDC\": 0, \"AVAX/USD:USDC\": 2, \"SOL/USD:USDC\": 1, \"XRP/USD:USDC\": 0,\n    \"SAND/USD:USDC\": 0, \"APE/USD:USDC\": 1, \"SWEAT/USD:USDC\": 2,\n    \"ATOM/USD:USDC\": 1, \"EOS/USD:USDC\": 1, \"CHZ/USD:USDC\": 0, \"NEAR/USD:USDC\": 1,\n    \"BNB/USD:USDC\": 2, \"LDO/USD:USDC\": 1, \"LUNA/USD:USDC\": 1, \"APT/USD:USDC\": 2}\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/open_orders.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\ndef get_open_orders(context):\n    return context.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=context.symbol)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_tags.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\ndef get_tagged_orders(\n    ctx, tag, symbol=None, since: int or float = -1, until: int or float = -1\n):\n    return ctx.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(\n        symbol=symbol, tag=tag, since=since, until=until\n    )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom .limit_order import *\nfrom .market_order import *\nfrom .stop_loss_order import *\nfrom .trailing_market_order import *\nfrom .trailing_limit_order import *\nfrom .trailing_stop_loss_order import *\nfrom .scaled_order import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/create_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\n\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.modes.script_keywords.basic_keywords as basic_keywords\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Meta.Keywords.scripting_library.settings as settings\nimport tentacles.Meta.Keywords.scripting_library.orders.position_size as position_size\nimport tentacles.Meta.Keywords.scripting_library.orders.chaining as chaining\nimport tentacles.Meta.Keywords.scripting_library.orders.grouping as grouping\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_private_data as exchange_private_data\n\n\nasync def create_order_instance(\n    context,\n    side=None,\n    symbol=None,\n\n    order_amount=None,\n    order_target_position=None,\n\n    stop_loss_offset=None,\n    stop_loss_tag=None,\n    stop_loss_type=None,\n    stop_loss_group=False,\n    take_profit_offset=None,\n    take_profit_tag=None,\n    take_profit_type=None,\n    take_profit_group=False,\n\n    order_type_name=None,\n\n    order_offset=None,\n    order_min_offset=None,\n    order_max_offset=None,\n    order_limit_offset=None,  # todo\n\n    slippage_limit=None,\n    time_limit=None,\n\n    reduce_only=False,\n    post_only=False,  # Todo\n    tag=None,\n\n    group=None,\n    wait_for=None\n):\n    if not context.enable_trading or _paired_order_is_closed(context, group):\n        return []\n    async with context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.lock:\n        # ensure proper trader allow_artificial_orders value\n        settings.set_allow_artificial_orders(context, context.allow_artificial_orders)\n        unknown_portfolio_on_creation = wait_for is not None and any(o.is_open() for o in wait_for)\n        input_side = side\n        order_quantity, side = await _get_order_quantity_and_side(context, order_amount, order_target_position,\n                                                                  order_type_name, input_side, reduce_only,\n                                                                  unknown_portfolio_on_creation)\n\n        order_type, order_price, final_side, reduce_only, trailing_method, \\\n        min_offset_val, max_offset_val, order_limit_offset, limit_offset_val = \\\n            await _get_order_details(context, order_type_name, side, order_offset, reduce_only, order_limit_offset)\n\n        stop_loss_price = None if stop_loss_offset is None else await script_keywords.get_price_with_offset(\n            context, stop_loss_offset\n        )\n        take_profit_price = None if take_profit_offset is None else await script_keywords.get_price_with_offset(\n            context, take_profit_offset\n        )\n        # round down when not reduce only and up when reduce only to avoid letting small positions open\n        truncate = not reduce_only\n        return await _create_order(context=context, symbol=symbol, order_quantity=order_quantity,\n                                   order_price=order_price, tag=tag, order_type_name=order_type_name,\n                                   input_side=input_side, side=side, final_side=final_side,\n                                   order_type=order_type, order_min_offset=order_min_offset,\n                                   max_offset_val=max_offset_val, reduce_only=reduce_only, group=group,\n                                   stop_loss_price=stop_loss_price, stop_loss_tag=stop_loss_tag,\n                                   stop_loss_type=stop_loss_type, stop_loss_group=stop_loss_group,\n                                   take_profit_price=take_profit_price, take_profit_tag=take_profit_tag,\n                                   take_profit_type=take_profit_type, take_profit_group=take_profit_group,\n                                   wait_for=wait_for, truncate=truncate, order_amount=order_amount,\n                                   order_target_position=order_target_position)\n\n\nasync def _get_order_percents(context, order_amount, order_target_position, input_side, symbol):\n    order_pf_percent = None\n    if order_amount is not None:\n        quantity_type, quantity = script_keywords.parse_quantity(order_amount)\n        if quantity_type in (script_keywords.QuantityType.PERCENT, script_keywords.QuantityType.AVAILABLE_PERCENT):\n            order_pf_percent = order_amount\n        elif quantity_type in (script_keywords.QuantityType.DELTA, script_keywords.QuantityType.DELTA_BASE):\n            percent = await script_keywords.get_order_size_portfolio_percent(\n                context, quantity, input_side, symbol\n            )\n            order_pf_percent = f\"{float(percent)}{script_keywords.QuantityType.PERCENT.value}\"\n        else:\n            raise trading_errors.InvalidArgumentError(f\"Unsupported quantity for trading signals: {order_amount}\")\n    order_position_percent = None\n    if order_target_position is not None:\n        quantity_type, quantity = script_keywords.parse_quantity(order_target_position)\n        if quantity_type in (script_keywords.QuantityType.PERCENT,\n                             script_keywords.QuantityType.AVAILABLE_PERCENT):\n            # position out of pf % here\n            order_pf_percent = order_target_position\n        elif quantity_type is script_keywords.QuantityType.POSITION_PERCENT:\n            order_position_percent = order_target_position\n        elif quantity_type is script_keywords.QuantityType.DELTA:\n            percent = order_target_position * exchange_private_data.open_position_size(context) \\\n                * trading_constants.ONE_HUNDRED\n            order_position_percent = f\"{float(percent)}{script_keywords.QuantityType.POSITION_PERCENT.value}\"\n    return order_pf_percent, order_position_percent\n\n\ndef _paired_order_is_closed(context, group):\n    grouped_orders = [] if group is None else group.get_group_open_orders()\n    if group is not None and grouped_orders and all(order.is_closed() for order in grouped_orders):\n        return True\n    for order in context.just_created_orders:\n        if order is not None:\n            if isinstance(order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\\\n               and order.order_group == group and order.is_closed():\n                return True\n    return False\n\n\ndef _use_total_holding(order_type_name):\n    return _is_stop_order(order_type_name)\n\n\ndef _is_stop_order(order_type_name):\n    return \"stop\" in order_type_name\n\n\nasync def _get_order_quantity_and_side(context, order_amount, order_target_position,\n                                       order_type_name, side, reduce_only, unknown_portfolio_on_creation):\n    if order_amount is not None and order_target_position is not None:\n        raise trading_errors.InvalidArgumentError(\"order_amount and order_target_position can't be \"\n                                                  \"both given as parameter\")\n\n    use_total_holding = _use_total_holding(order_type_name)\n    is_stop_order = _is_stop_order(order_type_name)\n    # size based on amount\n    if side is not None and order_amount is not None:\n        # side\n        if side != trading_enums.TradeOrderSide.BUY.value and side != trading_enums.TradeOrderSide.SELL.value:\n            # we should skip that cause of performance\n            raise trading_errors.InvalidArgumentError(\n                f\"Side parameter needs to be {trading_enums.TradeOrderSide.BUY.value} \"\n                f\"or {trading_enums.TradeOrderSide.SELL.value} for your {order_type_name}.\")\n        return await position_size.get_amount(context, order_amount, side, reduce_only, is_stop_order,\n                                              use_total_holding=use_total_holding,\n                                              unknown_portfolio_on_creation=unknown_portfolio_on_creation), side\n\n    # size and side based on target position\n    if order_target_position is not None:\n        return await position_size.get_target_position(context, order_target_position, reduce_only, is_stop_order,\n                                                       use_total_holding=use_total_holding,\n                                                       unknown_portfolio_on_creation=unknown_portfolio_on_creation)\n\n    raise trading_errors.InvalidArgumentError(\"Either use side with amount or target_position.\")\n\n\nasync def _get_order_details(context, order_type_name, side, order_offset, reduce_only, order_limit_offset):\n    # order types\n    order_type = None\n    final_side = side\n    order_price = None\n    min_offset_val = None\n    max_offset_val = None\n    limit_offset_val = None\n    trailing_method = None\n\n    # normal order\n    if order_type_name == \"market\":\n        order_type = trading_enums.TraderOrderType.SELL_MARKET if side == trading_enums.TradeOrderSide.SELL.value \\\n            else trading_enums.TraderOrderType.BUY_MARKET\n        order_price = await script_keywords.get_price_with_offset(context, \"0\")\n        final_side = None  # needs to be None\n\n    elif order_type_name == \"limit\":\n        order_type = trading_enums.TraderOrderType.SELL_LIMIT if side == trading_enums.TradeOrderSide.SELL.value \\\n            else trading_enums.TraderOrderType.BUY_LIMIT\n        order_price = await script_keywords.get_price_with_offset(context, order_offset)\n        final_side = None  # needs to be None\n        # todo post only\n\n    # conditional orders\n    # should be a real SL on the exchange short and long\n    elif order_type_name == \"stop_loss\":\n        order_type = trading_enums.TraderOrderType.STOP_LOSS\n        final_side = trading_enums.TradeOrderSide.SELL if side == trading_enums.TradeOrderSide.SELL.value \\\n            else trading_enums.TradeOrderSide.BUY\n        order_price = await script_keywords.get_price_with_offset(context, order_offset)\n        reduce_only = True\n\n    # should be conditional order on the exchange\n    elif order_type_name == \"stop_market\":\n        order_type = None  # todo\n        order_price = await script_keywords.get_price_with_offset(context, order_offset)\n\n    # has a trigger price and a offset where the limit gets placed when triggered -\n    # conditional order on exchange possible?\n    elif order_type_name == \"stop_limit\":\n        order_type = None  # todo\n        order_price = await script_keywords.get_price_with_offset(context, order_offset)\n        order_limit_offset = await script_keywords.get_price_with_offset(context, order_offset)\n        # todo post only\n\n    # trailling orders\n    # should be a real trailing stop loss on the exchange - short and long\n    elif order_type_name == \"trailing_stop_loss\":\n        order_price = await script_keywords.get_price_with_offset(context, order_offset)\n        order_type = None  # todo\n        reduce_only = True\n        trailing_method = \"continuous\"\n        # todo make sure order gets replaced by market if price jumped below price before order creation\n\n    # todo should use trailing on exchange if available or replace order on exchange\n    elif order_type_name == \"trailing_market\":\n        order_price = await script_keywords.get_price_with_offset(context, order_offset)\n        trailing_method = \"continuous\"\n        order_type = trading_enums.TraderOrderType.TRAILING_STOP\n        final_side = trading_enums.TradeOrderSide.SELL if side == trading_enums.TradeOrderSide.SELL.value \\\n            else trading_enums.TradeOrderSide.BUY\n\n    # todo should use trailing on exchange if available or replace order on exchange\n    elif order_type_name == \"trailing_limit\":\n        order_type = trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n        final_side = trading_enums.TradeOrderSide.SELL if side == trading_enums.TradeOrderSide.SELL.value \\\n            else trading_enums.TradeOrderSide.BUY\n        trailing_method = \"continuous\"\n        min_offset_val = await script_keywords.get_price_with_offset(context, order_offset)\n        # todo If the price changes such that the order becomes more than maxOffset away from the\n        #  price, then the order will be moved to minOffset away again.\n        max_offset_val = await script_keywords.get_price_with_offset(context, order_offset)\n        # todo post only\n\n    return order_type, order_price, final_side, reduce_only, trailing_method, \\\n           min_offset_val, max_offset_val, order_limit_offset, limit_offset_val\n\n\nasync def _create_order(context, symbol, order_quantity, order_price, tag, order_type_name, input_side, side,\n                        final_side,\n                        order_type, order_min_offset, max_offset_val, reduce_only, group,\n                        stop_loss_price, stop_loss_tag, stop_loss_type, stop_loss_group,\n                        take_profit_price, take_profit_tag, take_profit_type, take_profit_group,\n                        wait_for, truncate, order_amount, order_target_position):\n    # todo handle offsets, reduce_only, post_only,\n    orders = []\n    error_message = \"\"\n    chained_orders_group = _get_group_or_default(context, group, stop_loss_price, take_profit_price)\n    order_pf_percent = order_position_percent = None\n    if basic_keywords.is_emitting_trading_signals(context):\n        order_pf_percent, order_position_percent = await _get_order_percents(context, order_amount,\n                                                                             order_target_position, input_side, symbol)\n    try:\n        fees_currency_side = None\n        if context.exchange_manager.is_future:\n            fees_currency_side = context.exchange_manager.exchange.get_pair_future_contract(symbol).\\\n                get_fees_currency_side()\n        _, _, _, current_price, symbol_market = \\\n            await trading_personal_data.get_pre_order_data(context.exchange_manager,\n                                                           symbol=symbol,\n                                                           timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n        group_adapted_quantity = _get_group_adapted_quantity(context, group, order_type, order_quantity)\n        for final_order_quantity, final_order_price in \\\n                trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    group_adapted_quantity,\n                    order_price,\n                    symbol_market,\n                    truncate=truncate\n                ):\n            if not truncate:\n                # ensure enough money to trade (because of upper rounding)\n                available_acc_bal = await script_keywords.available_account_balance(\n                    context, side, use_total_holding=_use_total_holding(order_type_name),\n                    is_stop_order=_is_stop_order(order_type_name), reduce_only=reduce_only)\n                if final_order_quantity > available_acc_bal:\n                    final_order_quantity = trading_personal_data.decimal_adapt_quantity(\n                        symbol_market, available_acc_bal, truncate=True\n                    )\n            created_order = trading_personal_data.create_order_instance(\n                trader=context.trader,\n                order_type=order_type,\n                symbol=symbol,\n                current_price=current_price,\n                quantity=final_order_quantity,\n                price=final_order_price,\n                side=final_side,\n                tag=tag,\n                group=group,\n                reduce_only=reduce_only,\n                fees_currency_side=fees_currency_side\n            )\n            if order_min_offset is not None:\n                await created_order.set_trailing_percent(order_min_offset)\n            if wait_for:\n                chained_orders = await chaining.chain_order(wait_for, created_order)\n            else:\n                stop_loss_take_profit_quantity = final_order_quantity\n                fees = created_order.get_computed_fee()\n                if fees[trading_enums.FeePropertyColumns.CURRENCY.value] == created_order.quantity_currency:\n                    stop_loss_take_profit_quantity = final_order_quantity - \\\n                                                     fees[trading_enums.FeePropertyColumns.COST.value]\n                    stop_loss_take_profit_quantity = trading_personal_data.decimal_adapt_quantity(\n                        symbol_market, stop_loss_take_profit_quantity, truncate=True\n                    )\n                params = await _bundle_stop_loss_and_take_profit(\n                    context, symbol_market, fees_currency_side, created_order, stop_loss_take_profit_quantity,\n                    chained_orders_group,\n                    stop_loss_tag, stop_loss_type, stop_loss_price, stop_loss_group,\n                    take_profit_tag, take_profit_type, take_profit_price, take_profit_group,\n                    order_pf_percent, order_position_percent)\n                chained_orders = created_order.chained_orders\n                created_order = await context.trader.create_order(created_order, params=params)\n            if basic_keywords.is_emitting_trading_signals(context):\n                context.get_signal_builder().add_created_order(created_order, context.trader.exchange_manager,\n                                                               order_pf_percent, order_position_percent)\n            created_chained_orders = [order\n                                      for order in chained_orders\n                                      if order.is_created()]\n            # add chained order if any\n            context.just_created_orders += created_chained_orders\n            if wait_for:\n                # base order to be created are actually the chained orders, return them if created\n                orders += created_chained_orders\n            else:\n                # add create base order\n                orders.append(created_order)\n                context.just_created_orders.append(created_order)\n    except (trading_errors.MissingFunds, trading_errors.MissingMinimalExchangeTradeVolume):\n        error_message = \"missing minimal funds\"\n    except asyncio.TimeoutError as e:\n        error_message = f\"{e} and is necessary to compute the order details\"\n    except Exception as e:\n        error_message = f\"failed to create order : {e}.\"\n        context.logger.exception(e, True, f\"Failed to create order : {e}.\")\n    if not orders:\n        error_message = f\"not enough funds\"\n    if error_message:\n        context.logger.warning(f\"No order created when asking for {symbol} {order_type.name} \"\n                               f\"with a volume of {order_quantity} on {context.exchange_manager.exchange_name}: \"\n                               f\"{error_message}.\")\n    return orders\n\n\ndef _get_group_adapted_quantity(context, group, order_type, order_quantity):\n    if isinstance(group, trading_personal_data.BalancedTakeProfitAndStopOrderGroup) and context.just_created_orders:\n        all_take_profit = all_stop = True\n        is_creating_stop_order = trading_personal_data.is_stop_order(order_type)\n\n        for order in context.just_created_orders:\n            if order.order_group == group:\n                if trading_personal_data.is_stop_order(order.order_type):\n                    all_take_profit = False\n                else:\n                    all_stop = False\n        if (is_creating_stop_order and all_stop) or (not is_creating_stop_order and all_take_profit):\n            # we are only creating stop / take profit orders, no need to balance\n            return order_quantity\n        # we are now adding the order side of the orders, we need to balance\n        if group.can_create_order(order_type, order_quantity):\n            return order_quantity\n        return group.get_max_order_quantity(order_type)\n    return order_quantity\n\n\ndef _get_group_or_default(context, group, stop_loss_price, take_profit_price):\n    if stop_loss_price is not None or take_profit_price is not None:\n        # orders have to be bundled together, group them\n        if group is None:\n            # use balanced group by default\n            return grouping.create_balanced_take_profit_and_stop_group(context)\n        else:\n            return group\n    return group\n\n\nasync def _bundle_stop_loss_and_take_profit(\n        context, symbol_market, fees_currency_side, order, quantity, default_group,\n        stop_loss_tag, stop_loss_type, stop_loss_price, stop_loss_group,\n        take_profit_tag, take_profit_type, take_profit_price, take_profit_group,\n        order_pf_percent, order_position_percent) -> dict:\n    params = {}\n    side = trading_enums.TradeOrderSide.SELL if order.side is trading_enums.TradeOrderSide.BUY \\\n        else trading_enums.TradeOrderSide.BUY\n    order_kwargs = {\n        \"fees_currency_side\": fees_currency_side,\n        \"reduce_only\": True\n    }\n    if stop_loss_price is not None:\n        order_type = stop_loss_type if stop_loss_type else trading_enums.TraderOrderType.STOP_LOSS\n        params.update(\n            await _bundle_chained_order(context, symbol_market, order, quantity, default_group, side, order_kwargs,\n                                        stop_loss_tag, order_type, stop_loss_price, stop_loss_group,\n                                        order_pf_percent, order_position_percent)\n        )\n    if take_profit_price is not None:\n        if take_profit_type:\n            order_type = take_profit_type\n        else:\n            order_type = trading_enums.TraderOrderType.BUY_LIMIT if side is trading_enums.TradeOrderSide.BUY \\\n                else trading_enums.TraderOrderType.SELL_LIMIT\n        params.update(\n            await _bundle_chained_order(context, symbol_market, order, quantity, default_group, None, order_kwargs,\n                                        take_profit_tag, order_type, take_profit_price, take_profit_group,\n                                        order_pf_percent, order_position_percent)\n        )\n    return params\n\n\nasync def _bundle_chained_order(context, symbol_market, order, quantity, default_group, side, order_kwargs,\n                                tag, order_type, price, group, order_pf_percent, order_position_percent) -> dict:\n    adapted_price = trading_personal_data.decimal_adapt_price(symbol_market, price)\n    group = default_group if group is None else group\n    chained_order = trading_personal_data.create_order_instance(\n        trader=context.trader,\n        order_type=order_type,\n        symbol=order.symbol,\n        current_price=order.created_last_price,\n        quantity=quantity,\n        price=adapted_price,\n        side=side,\n        tag=tag,\n        group=group,\n        **order_kwargs\n    )\n    params = await context.trader.bundle_chained_order_with_uncreated_order(\n        order, chained_order, chained_order.update_with_triggering_order_fees\n    )\n    if basic_keywords.is_emitting_trading_signals(context):\n        context.get_signal_builder().add_created_order(chained_order, context.trader.exchange_manager,\n                                                       order_pf_percent, order_position_percent)\n    return params\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/limit_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\n\n\nasync def limit(\n    context,\n    side=None,\n    symbol=None,\n\n    amount=None,\n    target_position=None,\n\n    offset=None,\n\n    stop_loss_offset=None,\n    stop_loss_tag=None,\n    stop_loss_type=None,\n    stop_loss_group=None,\n    take_profit_offset=None,\n    take_profit_tag=None,\n    take_profit_type=None,\n    take_profit_group=None,\n\n    slippage_limit=None,\n    time_limit=None,\n\n    reduce_only=False,\n    post_only=False,\n    tag=None,\n\n    group=None,\n    wait_for=None\n):\n    return await create_order.create_order_instance(\n        context,\n        side=side,\n        symbol=symbol or context.symbol,\n\n        order_amount=amount,\n        order_target_position=target_position,\n\n        stop_loss_offset=stop_loss_offset,\n        stop_loss_tag=stop_loss_tag,\n        stop_loss_type=stop_loss_type,\n        stop_loss_group=stop_loss_group,\n        take_profit_offset=take_profit_offset,\n        take_profit_tag=take_profit_tag,\n        take_profit_type=take_profit_type,\n        take_profit_group=take_profit_group,\n\n        order_type_name=\"limit\",\n        order_offset=offset,\n\n        slippage_limit=slippage_limit,\n        time_limit=time_limit,\n        reduce_only=reduce_only,\n        post_only=post_only,\n\n        tag=tag,\n        group=group,\n        wait_for=wait_for\n    )\n\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/market_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\n\n\nasync def market(\n    context,\n    side=None,\n    symbol=None,\n\n    amount=None,\n    target_position=None,\n\n    stop_loss_offset=None,\n    stop_loss_tag=None,\n    stop_loss_type=None,\n    stop_loss_group=None,\n    take_profit_offset=None,\n    take_profit_tag=None,\n    take_profit_type=None,\n    take_profit_group=None,\n\n    reduce_only=False,\n\n    tag=None,\n\n    group=None,\n    wait_for=None\n):\n    return await create_order.create_order_instance(\n        context,\n        side=side,\n        symbol=symbol or context.symbol,\n\n        order_amount=amount,\n        order_target_position=target_position,\n\n        stop_loss_offset=stop_loss_offset,\n        stop_loss_tag=stop_loss_tag,\n        stop_loss_type=stop_loss_type,\n        stop_loss_group=stop_loss_group,\n        take_profit_offset=take_profit_offset,\n        take_profit_tag=take_profit_tag,\n        take_profit_type=take_profit_type,\n        take_profit_group=take_profit_group,\n\n        order_type_name=\"market\",\n\n        reduce_only=reduce_only,\n\n        tag=tag,\n        group=group,\n        wait_for=wait_for\n    )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/scaled_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.position_size as position_size\nimport octobot_trading.modes.script_keywords as script_keywords\n\nasync def scaled_limit(\n        context,\n        side=None,\n        symbol=None,\n\n        order_type_name=\"limit\",\n\n        scale_from=None,\n        scale_to=None,\n        order_count=10,\n        distribution=\"linear\",\n\n        amount=None,\n        target_position=None,\n\n        stop_loss_offset=None,\n        stop_loss_tag=None,\n        stop_loss_type=None,\n        stop_loss_group=None,\n        take_profit_offset=None,\n        take_profit_tag=None,\n        take_profit_type=None,\n        take_profit_group=None,\n\n        slippage_limit=None,\n        time_limit=None,\n\n        reduce_only=False,\n        post_only=False,\n\n        tag=None,\n\n        group=None,\n        wait_for=None\n):\n    amount_per_order = None\n    unknown_portfolio_on_creation = wait_for is not None\n    if target_position is None and amount is not None:\n        amount_per_order = await position_size. \\\n            get_amount(context, amount, side=side, use_total_holding=True,\n                       unknown_portfolio_on_creation=unknown_portfolio_on_creation) / order_count\n\n    elif target_position is not None and amount is None and side is None:\n        total_amount, side = await position_size.get_target_position(\n            context, target_position, reduce_only=reduce_only,\n            unknown_portfolio_on_creation=unknown_portfolio_on_creation)\n        amount_per_order = total_amount / order_count\n    else:\n        raise RuntimeError(\"Either use side with amount or target_position for scaled orders.\")\n\n    scale_from_price = await script_keywords.get_price_with_offset(context, scale_from, side=side)\n    scale_to_price = await script_keywords.get_price_with_offset(context, scale_to, side=side)\n    order_prices = []\n    if distribution == \"linear\":\n        if scale_from_price >= scale_to_price:\n            price_difference = scale_from_price - scale_to_price\n            step_size = price_difference / (order_count - 1)\n            for i in range(0, order_count):\n                order_prices.append(scale_from_price - (step_size * i))\n        elif scale_to_price > scale_from_price:\n            price_difference = scale_to_price - scale_from_price\n            step_size = price_difference / (order_count - 1)\n            for i in range(0, order_count):\n                order_prices.append(scale_from_price + (step_size * i))\n\n    else:\n        raise RuntimeError(\"scaled order: unsupported distribution type. check the documentation for more informations\")\n    created_orders = []\n    for order_price in order_prices:\n        new_created_order = await create_order.create_order_instance(\n            context,\n            side=side,\n            symbol=symbol or context.symbol,\n\n            order_amount=amount_per_order,\n\n            order_type_name=\"limit\",\n            order_offset=f\"@{order_price}\",\n\n            stop_loss_offset=stop_loss_offset,\n            stop_loss_tag=stop_loss_tag,\n            stop_loss_type=stop_loss_type,\n            stop_loss_group=stop_loss_group,\n            take_profit_offset=take_profit_offset,\n            take_profit_tag=take_profit_tag,\n            take_profit_type=take_profit_type,\n            take_profit_group=take_profit_group,\n\n            slippage_limit=slippage_limit,\n            time_limit=time_limit,\n\n            reduce_only=reduce_only,\n            post_only=post_only,\n            group=group,\n            tag=tag,\n\n            wait_for=wait_for\n        )\n        try:\n            created_orders.append(new_created_order[0])\n        except IndexError:\n            pass\n            # raise RuntimeError(f\"scaled {side} order not created\")\n    return created_orders\n\n\nasync def scaled_stop_loss(\n        context,\n        side=None,\n        symbol=None,\n\n        scale_from=None,\n        scale_to=None,\n        order_count=10,\n        distribution=\"linear\",\n\n        amount=None,\n        target_position=None,\n\n        slippage_limit=None,\n        time_limit=None,\n\n        tag=None,\n\n        group=None,\n        wait_for=None\n):\n    await scaled_limit(context,\n                       side=side,\n                       symbol=symbol,\n\n                       order_type_name=\"stop_loss\",\n\n                       scale_from=scale_from,\n                       scale_to=scale_to,\n                       order_count=order_count,\n                       distribution=distribution,\n\n                       amount=amount,\n                       target_position=target_position,\n\n                       slippage_limit=slippage_limit,\n                       time_limit=time_limit,\n\n                       reduce_only=True,\n\n                       tag=tag,\n\n                       group=group,\n                       wait_for=wait_for\n                       )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/stop_loss_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\n\n\nasync def stop_loss(\n    context,\n    side=None,\n    symbol=None,\n\n    offset=None,\n\n    amount=None,\n    target_position=None,\n\n    tag=None,\n\n    group=None,\n    wait_for=None\n):\n    return await create_order.create_order_instance(\n        context,\n        side=side,\n        symbol=symbol or context.symbol,\n\n        order_amount=amount,\n        order_target_position=target_position,\n\n        order_type_name=\"stop_loss\",\n        order_offset=offset,\n\n        reduce_only=True,\n        tag=tag,\n\n        group=group,\n        wait_for=wait_for\n    )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/trailing_limit_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\n\n\nasync def trailing_limit(\n    context,\n    side=None,\n    symbol=None,\n\n    amount=None,\n    target_position=None,\n\n    offset=None,\n    min_offset=None,\n    max_offset=None,\n\n    slippage_limit=None,\n    time_limit=None,\n\n    reduce_only=False,\n    post_only=False,\n\n    tag=None,\n\n    group=None,\n    wait_for=None\n):\n    return await create_order.create_order_instance(\n        context,\n        side=side,\n        symbol=symbol or context.symbol,\n\n        order_amount=amount,\n        order_target_position=target_position,\n\n        order_type_name=\"trailing_limit\",\n\n        order_min_offset=min_offset,\n        order_max_offset=max_offset,\n        order_offset=offset,\n\n        slippage_limit=slippage_limit,\n        time_limit=time_limit,\n        reduce_only=reduce_only,\n        post_only=post_only,\n        group=group,\n\n        tag=tag,\n\n\n        wait_for=wait_for\n    )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/trailing_market_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\n\n\nasync def trailing_market(\n    context,\n    side=None,\n    symbol=None,\n\n    amount=None,\n    target_position=None,\n\n    offset=None,\n\n    reduce_only=False,\n\n    tag=None,\n\n    group=None,\n    wait_for=None\n):\n    return await create_order.create_order_instance(\n        context,\n        side=side,\n        symbol=symbol or context.symbol,\n\n        order_amount=amount,\n        order_target_position=target_position,\n\n        order_type_name=\"trailing_market\",\n\n        order_offset=offset,\n\n        reduce_only=reduce_only,\n\n        tag=tag,\n        group=group,\n        wait_for=wait_for\n    )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/order_types/trailing_stop_loss_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\n\n\nasync def trailing_stop_loss(\n    context,\n    side=None,\n    symbol=None,\n\n    amount=None,\n    target_position=None,\n\n    offset=None,\n\n    reduce_only=True,\n    tag=None,\n\n    group=None,\n    wait_for=None\n) -> list:\n    return await create_order.create_order_instance(\n        context,\n        side=side,\n        symbol=symbol or context.symbol,\n\n        order_amount=amount,\n        order_target_position=target_position,\n\n        order_type_name=\"trailing_stop_loss\",\n\n        order_offset=offset,\n\n        reduce_only=reduce_only,\n\n        tag=tag,\n        group=group,\n\n        wait_for=wait_for\n    )\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/position_size/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom .amount import *\nfrom .target_position import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/position_size/amount.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.modes.script_keywords as script_keywords\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.enums as trading_enums\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_private_data as exchange_private_data\nimport octobot_commons.constants as commons_constants\n\n\nasync def get_amount(\n    context=None,\n    input_amount=None,\n    side=trading_enums.TradeOrderSide.BUY.value,\n    reduce_only=True,\n    is_stop_order=False,\n    use_total_holding=False,\n    unknown_portfolio_on_creation=False,\n    target_price=None\n):\n    amount_value = await script_keywords.get_amount_from_input_amount(\n        context=context,\n        input_amount=input_amount,\n        side=side,\n        reduce_only=reduce_only,\n        is_stop_order=is_stop_order,\n        use_total_holding=use_total_holding,\n        target_price=target_price\n    )\n    if unknown_portfolio_on_creation:\n        # no way to check if the amount is valid when creating order\n        _, amount_value = script_keywords.parse_quantity(input_amount)\n    return amount_value\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/position_size/target_position.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_private_data as exchange_private_data\n\n\n# todo handle negative open position for shorts\nasync def get_target_position(\n        context=None,\n        target=None,\n        reduce_only=True,\n        is_stop_order=False,\n        use_total_holding=False,\n        unknown_portfolio_on_creation=False,\n        target_price=None\n):\n    target_position_type, target_position_value = script_keywords.parse_quantity(target)\n\n    if target_position_type is script_keywords.QuantityType.POSITION_PERCENT:\n        open_position_size_val = exchange_private_data.open_position_size(context)\n        target_size = open_position_size_val * target_position_value / 100\n        order_size = target_size - open_position_size_val\n\n    elif target_position_type is script_keywords.QuantityType.PERCENT:\n        total_acc_bal = await script_keywords.total_account_balance(context)\n        target_size = total_acc_bal * target_position_value / 100\n        order_size = target_size - exchange_private_data.open_position_size(context)\n\n    # in target position, we always provide the position size we want to end up with\n    elif target_position_type in (script_keywords.QuantityType.DELTA, script_keywords.QuantityType.DELTA_BASE) \\\n            or target_position_type is script_keywords.QuantityType.FLAT:\n        order_size = target_position_value - exchange_private_data.open_position_size(context)\n        if target == order_size:\n            # no order to create\n            return trading_constants.ZERO, trading_enums.TradeOrderSide.BUY.value\n\n    elif target_position_type is script_keywords.QuantityType.AVAILABLE_PERCENT:\n        available_account_balance_val = await script_keywords.available_account_balance(context,\n                                                                                        reduce_only=reduce_only)\n        order_size = available_account_balance_val * target_position_value / 100\n\n    else:\n        raise trading_errors.InvalidArgumentError(\"make sure to use a supported syntax for position\")\n\n    side = get_target_position_side(order_size)\n    if side == trading_enums.TradeOrderSide.SELL.value:\n        order_size = order_size * -1\n    if not unknown_portfolio_on_creation:\n        order_size = await script_keywords.adapt_amount_to_holdings(context, order_size, side,\n                                                                    use_total_holding, reduce_only, is_stop_order,\n                                                                    target_price=target_price)\n    return order_size, side\n\n\ndef get_target_position_side(order_size):\n    if order_size < 0:\n        return trading_enums.TradeOrderSide.SELL.value\n    elif order_size > 0:\n        return trading_enums.TradeOrderSide.BUY.value\n    # order_size == 0\n    raise RuntimeError(\"Computed position size is 0\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/orders/waiting.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport time\n\nimport tentacles.Meta.Keywords.scripting_library.orders.open_orders as open_orders\nimport octobot_trading.personal_data as personal_data\nimport octobot_commons.logging as logging\n\n\nasync def wait_for_orders_close(ctx, orders, timeout=None):\n    if not isinstance(orders, list):\n        orders = [orders]\n    t0 = time.time()\n    refresh_interval = 0.01\n    # wait for orders to be filled or cancelled\n    # also wait for associated chained orders to be opened\n    try:  # order.is_closed() fails when order got filled meanwhile\n        while not all(order.is_closed() for order in orders) or \\\n                not are_all_chained_orders_created(ctx, orders):\n            if timeout is None or time.time() - t0 < timeout:\n                if ctx.exchange_manager.is_backtesting:\n                    raise asyncio.TimeoutError(\"Can't wait for orders in backtesting\")\n                await asyncio.sleep(refresh_interval)\n            else:\n                raise asyncio.TimeoutError(\"Order wasnt not filled in time\")\n    except AttributeError as e:\n        logging.get_logger(\"Waiting\").exception(e, True, \"AttributeError on checking orders (should not happen)\")\n        pass  # continue try to create take profit in case of connection issues\n\n\ndef are_all_chained_orders_created(ctx, orders):\n    for order in orders:\n        for chained_order in order.chained_orders:\n            if not chained_order.is_created():\n                return False\n            if chained_order.is_closed():\n                continue\n            found_order = False\n            # ensure that chained orders are open or got closed\n            for open_order in open_orders.get_open_orders(ctx):\n                if personal_data.is_associated_pending_order(open_order, chained_order):\n                    found_order = True\n                    break\n            if not found_order:\n                return False\n    return True\n\n\nasync def wait_for_stop_loss_open(ctx, order_tag=None, order_group=None, timeout=60):\n    \"\"\"\n    waits for and finds a stop order based on order tag or order group\n    :param ctx:\n    :param order_tag:\n    :param order_group:\n    :param timeout: in seconds\n    :return: the stop loss order\n    \"\"\"\n    t0 = time.time()\n    refresh_interval = 0.01\n    orders = ctx.exchange_manager.exchange_personal_data.orders_manager.orders\n\n    stop_found = False\n    while not stop_found:\n        for order in orders:\n            stop_found = orders[order].tag == order_tag or orders[order].order_group == order_group\n            if stop_found:\n                return orders[order]\n        if timeout is None or time.time() - t0 < timeout:\n            if ctx.exchange_manager.is_backtesting:\n                raise asyncio.TimeoutError(\"Can't wait for orders in backtesting\")\n            await asyncio.sleep(refresh_interval)\n        else:\n            ctx.logger.error(\"Stop Loss Order was not found: was not placed in time or got already triggered\")\n            return None\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/settings/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom .script_settings import *\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/settings/script_settings.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.api as trading_api\nimport octobot_commons.errors as errors\n\n\ndef set_minimum_candles(context, candles_count):\n    available_candles = 0\n    try:\n        available_candles = trading_api.get_symbol_candles_count(\n            trading_api.get_symbol_data(context.exchange_manager, context.symbol, allow_creation=False),\n            context.time_frame\n        )\n        if available_candles >= candles_count:\n            return\n    except KeyError:\n        pass\n    raise errors.MissingDataError(f\"Missing candles: available: {available_candles}, required: {candles_count}\")\n\n\ndef do_not_initialize():\n    raise errors.MissingDataError(\"Script should not be considered initialized (do_not_initialize call)\")\n\n\ndef set_allow_artificial_orders(context, allow_artificial_orders):\n    context.allow_artificial_orders = allow_artificial_orders\n    context.exchange_manager.trader.allow_artificial_orders = context.allow_artificial_orders\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport pytest_asyncio\nimport mock\nimport decimal\nimport sys\nimport asyncio\n\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_trading.modes.script_keywords.context_management as context_management\nimport octobot_trading.exchanges as trading_exchanges\nimport octobot_trading.enums as enums\n\n\n@pytest.fixture\ndef null_context():\n    context = context_management.Context(\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    )\n    yield context\n\n\n@pytest_asyncio.fixture\nasync def mock_context(backtesting_trader):\n    _, exchange_manager, trader_inst = backtesting_trader\n    context = context_management.Context(\n        mock.Mock(),\n        exchange_manager,\n        trader_inst,\n        mock.Mock(),\n        \"BTC/USDT\",\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n        mock.Mock(),\n    )\n    context.signal_builder = mock.Mock()\n    context.is_trading_signal_emitter = mock.Mock(return_value=False)\n    context.orders_writer = mock.Mock(log_many=mock.AsyncMock())\n    portfolio_manager = exchange_manager.exchange_personal_data.portfolio_manager\n    # init portfolio with 0.5 BTC, 20 ETH and 30000 USDT and only 0.1 available BTC\n    portfolio_manager.portfolio.update_portfolio_from_balance({\n        'BTC': {'available': decimal.Decimal(\"0.1\"), 'total': decimal.Decimal(\"0.5\")},\n        'ETH': {'available': decimal.Decimal(\"20\"), 'total': decimal.Decimal(\"20\")},\n        'USDT': {'available': decimal.Decimal(\"30000\"), 'total': decimal.Decimal(\"30000\")}\n    }, True)\n    exchange_manager.client_symbols.append(\"BTC/USDT\")\n    exchange_manager.client_symbols.append(\"ETH/USDT\")\n    exchange_manager.client_symbols.append(\"ETH/BTC\")\n    # init prices with BTC/USDT = 40000, ETH/BTC = 0.1 and ETH/USDT = 4000\n    portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\"BTC/USDT\"] = \\\n        decimal.Decimal(\"40000\")\n    portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\"ETH/USDT\"] = \\\n        decimal.Decimal(\"4000\")\n    portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\"ETH/BTC\"] = \\\n        decimal.Decimal(\"0.1\")\n    portfolio_manager.handle_balance_updated()\n    yield context\n\n\n@pytest.fixture\ndef symbol_market():\n    return {\n        enums.ExchangeConstantsMarketStatusColumns.LIMITS.value: {\n            enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT.value: {\n                enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT_MIN.value: 0.5,\n                enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT_MAX.value: 100,\n            },\n            enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST.value: {\n                enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value: 1,\n                enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MAX.value: 200\n            },\n            enums.ExchangeConstantsMarketStatusColumns.LIMITS_PRICE.value: {\n                enums.ExchangeConstantsMarketStatusColumns.LIMITS_PRICE_MIN.value: 0.5,\n                enums.ExchangeConstantsMarketStatusColumns.LIMITS_PRICE_MAX.value: 50\n            },\n        },\n        enums.ExchangeConstantsMarketStatusColumns.PRECISION.value: {\n            enums.ExchangeConstantsMarketStatusColumns.PRECISION_PRICE.value: 8,\n            enums.ExchangeConstantsMarketStatusColumns.PRECISION_AMOUNT.value: 8\n        }\n    }\n\n\n@pytest.fixture\ndef event_loop():\n    # re-configure async loop each time this fixture is called\n    _configure_async_test_loop()\n    loop = asyncio.new_event_loop()\n    # use ErrorContainer to catch otherwise hidden exceptions occurring in async scheduled tasks\n    error_container = asyncio_tools.ErrorContainer()\n    loop.set_exception_handler(error_container.exception_handler)\n    yield loop\n    # will fail if exceptions have been silently raised\n    loop.run_until_complete(error_container.check())\n    loop.close()\n\n\n@pytest.fixture\ndef skip_if_octobot_trading_mocking_disabled(request):\n    try:\n        with mock.patch.object(trading_exchanges.Trader, \"cancel_order\", mock.AsyncMock()):\n            pass\n        # mocking is available\n    except TypeError:\n        pytest.skip(reason=f\"Disabled {request.node.name} [OctoBot-Trading mocks not allowed]\")\n\n\ndef _configure_async_test_loop():\n    if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):\n        # use WindowsSelectorEventLoopPolicy to avoid aiohttp connexion close warnings\n        # https://github.com/encode/httpx/issues/914#issuecomment-622586610\n        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())\n\n\n# set default values for async loop\n_configure_async_test_loop()\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/backtesting/__init__.py",
    "content": ""
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/backtesting/data_store.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport octobot_trading.enums as trading_enums\nimport octobot_commons.enums as commons_enums\n\n\n@pytest.fixture\ndef default_price_data():\n    # imported from real backtesting data\n    return {\n        \"BTC/USDT\": [[1606780800000.0, 19695.87, 19720.0, 19479.8, 19565.47, 4570.361518], [1606784400000.0, 19565.47, 19639.99, 19433.15, 19605.75, 2702.459235], [1606788000000.0, 19605.75, 19704.93, 19548.57, 19680.95, 2408.229978], [1606791600000.0, 19680.96, 19682.77, 19340.0, 19419.74, 2889.848604], [1606795200000.0, 19419.73, 19527.02, 19344.92, 19354.31, 3400.857941], [1606798800000.0, 19352.64, 19502.54, 19281.38, 19483.73, 2620.883792], [1606802400000.0, 19483.73, 19517.94, 19309.87, 19338.34, 3129.776329], [1606806000000.0, 19338.33, 19546.81, 19300.0, 19515.63, 3009.225182], [1606809600000.0, 19515.62, 19567.0, 19441.19, 19466.99, 3143.172961], [1606813200000.0, 19467.0, 19570.0, 19426.96, 19565.0, 2824.268695], [1606816800000.0, 19564.99, 19800.0, 19558.77, 19739.51, 7640.260767], [1606820400000.0, 19739.51, 19888.0, 18886.0, 19425.0, 14556.657151], [1606824000000.0, 19425.4, 19482.01, 18399.99, 18551.35, 17554.24707], [1606827600000.0, 18550.25, 18844.15, 18001.12, 18759.73, 14772.777639], [1606831200000.0, 18759.74, 19364.82, 18651.0, 19285.31, 7821.047189], [1606834800000.0, 19285.3, 19489.3, 19147.6, 19263.37, 6838.162713], [1606838400000.0, 19263.36, 19325.83, 18938.22, 19058.8, 5690.017039], [1606842000000.0, 19058.8, 19086.95, 18611.88, 19058.4, 6526.028983], [1606845600000.0, 19058.4, 19074.64, 18693.37, 18738.82, 3316.505911], [1606849200000.0, 18738.83, 19135.19, 18720.48, 19069.79, 2954.651375], [1606852800000.0, 19069.79, 19150.0, 18862.0, 19024.32, 2451.419624], [1606856400000.0, 19024.33, 19211.0, 18936.61, 19038.39, 2275.27857], [1606860000000.0, 19038.39, 19156.72, 18830.18, 18895.0, 1719.522796], [1606863600000.0, 18895.01, 18943.26, 18725.0, 18764.96, 2883.10159], [1606867200000.0, 18764.96, 18877.92, 18433.0, 18836.51, 4372.162317], [1606870800000.0, 18836.5, 18972.12, 18703.0, 18854.01, 2504.979019], [1606874400000.0, 18854.01, 18863.73, 18639.86, 18676.4, 1647.545452], [1606878000000.0, 18676.39, 18788.32, 18506.16, 18618.25, 3284.666149], [1606881600000.0, 18618.0, 18700.0, 18330.0, 18550.96, 4330.094724], [1606885200000.0, 18550.95, 18660.0, 18465.42, 18654.41, 2661.915712], [1606888800000.0, 18654.27, 18939.97, 18630.63, 18926.66, 4068.001777], [1606892400000.0, 18924.13, 19135.0, 18833.0, 19124.48, 4325.750236], [1606896000000.0, 19124.49, 19342.0, 19059.09, 19195.19, 4980.517871], [1606899600000.0, 19195.52, 19319.67, 18977.03, 19035.99, 3695.078977], [1606903200000.0, 19036.29, 19196.63, 18991.43, 19103.39, 3344.073936], [1606906800000.0, 19103.38, 19199.0, 19022.47, 19127.31, 2688.864019], [1606910400000.0, 19127.31, 19129.13, 18792.31, 18940.98, 4128.997824], [1606914000000.0, 18940.97, 19250.7, 18919.31, 19124.51, 3955.239884], [1606917600000.0, 19126.75, 19175.0, 18850.0, 18910.21, 4219.856604], [1606921200000.0, 18910.21, 18989.0, 18728.38, 18891.57, 3966.239422], [1606924800000.0, 18891.57, 19015.7, 18770.0, 18856.25, 3304.383039], [1606928400000.0, 18856.25, 18999.0, 18810.27, 18976.33, 2378.363036], [1606932000000.0, 18976.33, 19068.0, 18894.8, 19011.99, 2505.872597], [1606935600000.0, 19011.97, 19139.06, 18964.08, 19101.1, 2128.498473], [1606939200000.0, 19101.1, 19150.0, 19044.85, 19083.4, 2075.778464], [1606942800000.0, 19083.77, 19168.91, 19046.3, 19145.01, 1486.371573], [1606946400000.0, 19145.0, 19235.0, 19099.0, 19111.13, 1845.354603], [1606950000000.0, 19111.13, 19260.0, 19089.5, 19204.09, 2012.40777], [1606953600000.0, 19204.08, 19299.0, 19150.76, 19180.0, 2131.560886], [1606957200000.0, 19180.0, 19184.54, 18940.0, 19016.91, 2664.789937], [1606960800000.0, 19016.92, 19099.05, 18945.0, 19041.73, 2011.154103], [1606964400000.0, 19041.73, 19178.87, 19013.17, 19087.0, 1658.093318], [1606968000000.0, 19087.01, 19124.03, 19022.22, 19041.21, 1398.143328], [1606971600000.0, 19041.21, 19110.0, 18867.2, 18922.83, 2207.439489], [1606975200000.0, 18922.83, 19015.42, 18880.51, 18970.24, 1915.073941], [1606978800000.0, 18970.24, 19250.0, 18911.0, 19208.11, 3187.186407], [1606982400000.0, 19208.11, 19450.0, 19161.85, 19378.79, 6548.804707], [1606986000000.0, 19378.79, 19420.96, 19097.6, 19363.99, 4473.862826], [1606989600000.0, 19363.98, 19422.9, 19244.75, 19353.69, 3404.982735], [1606993200000.0, 19353.69, 19444.4, 19290.0, 19396.5, 3752.006358], [1606996800000.0, 19396.49, 19425.0, 19219.78, 19321.26, 3073.255484], [1607000400000.0, 19321.26, 19375.16, 19252.2, 19281.0, 2397.631645], [1607004000000.0, 19281.51, 19397.0, 19274.0, 19351.32, 2196.785603], [1607007600000.0, 19351.32, 19536.2, 19300.0, 19535.0, 4242.672379], [1607011200000.0, 19535.0, 19598.0, 19251.76, 19354.78, 5994.113495], [1607014800000.0, 19354.78, 19384.23, 19194.06, 19299.23, 2612.551421], [1607018400000.0, 19299.24, 19398.9, 19299.24, 19371.46, 2030.270253], [1607022000000.0, 19371.46, 19422.9, 19328.51, 19414.29, 1747.888777], [1607025600000.0, 19414.29, 19435.29, 19306.27, 19369.44, 1970.71359], [1607029200000.0, 19369.44, 19462.24, 19328.92, 19438.86, 1585.05878], [1607032800000.0, 19438.86, 19479.71, 19402.0, 19464.11, 1224.310923], [1607036400000.0, 19464.12, 19540.0, 19402.11, 19421.9, 2261.040894], [1607040000000.0, 19422.34, 19527.0, 19378.92, 19460.65, 2083.900225], [1607043600000.0, 19462.49, 19489.84, 19319.39, 19321.42, 1937.056634], [1607047200000.0, 19323.31, 19375.0, 19250.0, 19251.92, 2016.847648], [1607050800000.0, 19251.92, 19323.31, 19122.74, 19162.62, 2645.78391], [1607054400000.0, 19162.62, 19318.83, 19122.48, 19286.78, 2332.879073], [1607058000000.0, 19286.78, 19312.79, 19190.52, 19200.07, 1607.201706], [1607061600000.0, 19200.01, 19367.05, 19192.89, 19317.13, 1791.575063], [1607065200000.0, 19317.13, 19335.83, 19238.31, 19283.94, 2191.960974], [1607068800000.0, 19283.94, 19447.14, 19281.23, 19388.89, 3152.638117], [1607072400000.0, 19388.9, 19410.49, 19316.11, 19354.23, 1738.767103], [1607076000000.0, 19354.23, 19360.73, 18900.0, 18978.35, 9630.663093], [1607079600000.0, 18978.35, 19077.16, 18700.0, 18833.25, 5226.950061], [1607083200000.0, 18834.48, 19029.56, 18686.38, 19026.49, 6651.625618], [1607086800000.0, 19026.49, 19027.0, 18914.42, 19005.34, 2476.86035], [1607090400000.0, 19005.34, 19146.22, 18917.84, 19046.11, 3301.095255], [1607094000000.0, 19046.11, 19073.46, 18938.25, 18943.35, 2288.005452], [1607097600000.0, 18944.06, 18991.7, 18817.0, 18981.28, 3493.465697], [1607101200000.0, 18981.28, 19029.2, 18932.23, 18968.93, 1898.7485], [1607104800000.0, 18968.82, 19078.68, 18929.16, 19056.45, 1718.020232], [1607108400000.0, 19056.45, 19065.0, 19000.0, 19038.73, 1689.617236], [1607112000000.0, 19038.73, 19045.34, 18880.0, 18962.53, 2390.289129], [1607115600000.0, 18962.52, 18988.75, 18725.6, 18806.41, 3265.941089], [1607119200000.0, 18807.09, 18875.27, 18565.31, 18665.3, 2805.203431], [1607122800000.0, 18665.31, 18841.0, 18601.5, 18650.52, 2948.572604], [1607126400000.0, 18650.51, 18791.53, 18500.0, 18764.23, 4398.592542], [1607130000000.0, 18764.23, 18819.83, 18634.5, 18644.89, 2253.931869], [1607133600000.0, 18644.88, 18848.62, 18641.1, 18789.66, 2388.321713], [1607137200000.0, 18789.65, 18880.0, 18738.34, 18818.85, 2317.888684], [1607140800000.0, 18818.05, 18932.0, 18800.0, 18863.9, 1551.894666], [1607144400000.0, 18863.9, 18970.0, 18840.69, 18954.42, 1647.778313], [1607148000000.0, 18955.83, 18990.08, 18906.16, 18963.03, 1590.750531], [1607151600000.0, 18963.03, 18986.61, 18900.0, 18911.31, 1514.204824], [1607155200000.0, 18911.3, 19177.0, 18911.3, 19149.9, 3390.549236], [1607158800000.0, 19149.9, 19165.0, 19053.05, 19110.42, 1671.420619], [1607162400000.0, 19110.42, 19137.28, 19037.5, 19114.48, 1404.535637], [1607166000000.0, 19114.48, 19127.46, 18959.03, 19013.43, 2031.724234], [1607169600000.0, 19013.43, 19080.01, 18965.45, 19060.01, 1505.479033], [1607173200000.0, 19060.27, 19069.9, 18941.74, 18983.13, 1663.18758], [1607176800000.0, 18982.57, 19092.03, 18963.83, 19080.01, 2031.09956], [1607180400000.0, 19080.01, 19150.0, 19051.0, 19110.78, 1878.881403], [1607184000000.0, 19111.13, 19149.92, 19044.28, 19099.0, 1712.332099], [1607187600000.0, 19098.99, 19125.76, 19026.0, 19125.76, 1130.566015], [1607191200000.0, 19125.77, 19140.0, 19085.0, 19116.3, 1113.481955], [1607194800000.0, 19116.65, 19172.37, 19075.3, 19119.19, 1273.081414], [1607198400000.0, 19119.19, 19140.0, 18993.92, 19054.9, 1291.846074], [1607202000000.0, 19055.24, 19110.0, 19000.0, 19007.04, 1050.880547], [1607205600000.0, 19007.04, 19052.17, 18977.16, 19020.5, 772.952693], [1607209200000.0, 19020.59, 19157.52, 19020.08, 19147.66, 1337.367332], [1607212800000.0, 19147.66, 19277.0, 19131.02, 19268.81, 2729.198654], [1607216400000.0, 19268.81, 19342.0, 19228.0, 19261.52, 2377.439344], [1607220000000.0, 19261.51, 19288.06, 19206.03, 19233.43, 1450.02811], [1607223600000.0, 19233.44, 19245.91, 19137.18, 19183.39, 1223.127577], [1607227200000.0, 19183.39, 19231.08, 19155.04, 19185.92, 881.834286], [1607230800000.0, 19185.92, 19222.54, 19105.0, 19151.97, 1054.988231], [1607234400000.0, 19152.0, 19224.66, 19133.5, 19183.94, 845.302981], [1607238000000.0, 19184.23, 19260.0, 19172.3, 19230.99, 921.805973], [1607241600000.0, 19230.99, 19251.0, 19003.64, 19017.56, 2003.080539], [1607245200000.0, 19017.54, 19083.98, 18950.87, 19042.22, 2087.157592], [1607248800000.0, 19042.23, 19061.53, 18957.04, 19041.31, 1124.016156], [1607252400000.0, 19041.32, 19100.0, 18959.73, 19089.16, 1115.628806], [1607256000000.0, 19089.15, 19089.16, 18960.55, 18982.47, 1111.226328], [1607259600000.0, 18982.87, 19021.0, 18857.0, 18953.07, 2100.601588], [1607263200000.0, 18953.07, 19131.53, 18930.98, 19120.0, 1809.57926], [1607266800000.0, 19120.27, 19152.38, 19094.0, 19125.83, 1350.286034], [1607270400000.0, 19125.84, 19213.09, 19018.0, 19106.8, 1927.444027], [1607274000000.0, 19106.46, 19177.16, 19065.91, 19128.02, 920.596848], [1607277600000.0, 19127.86, 19174.36, 19093.78, 19105.21, 870.086461], [1607281200000.0, 19105.2, 19156.96, 19076.47, 19132.62, 870.441944], [1607284800000.0, 19132.62, 19184.62, 19123.41, 19168.73, 868.814714], [1607288400000.0, 19168.73, 19244.0, 19114.01, 19238.08, 1606.570085], [1607292000000.0, 19238.08, 19249.0, 19045.0, 19125.44, 1481.428442], [1607295600000.0, 19125.55, 19420.0, 19125.55, 19359.4, 4312.407881], [1607299200000.0, 19358.67, 19420.91, 19288.23, 19318.56, 1983.516535], [1607302800000.0, 19318.56, 19349.55, 19219.99, 19293.08, 1527.856348], [1607306400000.0, 19293.09, 19307.49, 19169.35, 19200.0, 1548.269166], [1607310000000.0, 19200.01, 19288.23, 19157.55, 19282.68, 1339.849597], [1607313600000.0, 19282.68, 19306.11, 19233.36, 19285.97, 1193.985926], [1607317200000.0, 19285.97, 19299.0, 19240.0, 19246.26, 1023.882776], [1607320800000.0, 19246.25, 19356.3, 19237.37, 19306.36, 1454.812033], [1607324400000.0, 19306.36, 19399.0, 19293.93, 19371.3, 1503.212608], [1607328000000.0, 19371.24, 19386.15, 19200.0, 19250.84, 1808.057972], [1607331600000.0, 19250.85, 19256.62, 19177.99, 19213.34, 1678.482081], [1607335200000.0, 19213.34, 19254.06, 19147.05, 19183.41, 1839.598649], [1607338800000.0, 19183.41, 19231.22, 19090.0, 19103.58, 1914.160878], [1607342400000.0, 19103.79, 19239.0, 19097.5, 19238.8, 2301.613075], [1607346000000.0, 19239.0, 19253.09, 19180.7, 19187.67, 1522.342358], [1607349600000.0, 19187.9, 19234.61, 19095.72, 19195.84, 2077.875894], [1607353200000.0, 19195.84, 19257.03, 19168.88, 19213.9, 1660.035444], [1607356800000.0, 19213.9, 19241.39, 19166.79, 19188.36, 1587.897438], [1607360400000.0, 19188.29, 19210.09, 19139.99, 19160.01, 1440.248622], [1607364000000.0, 19160.01, 19188.16, 18902.88, 18939.7, 4582.186702], [1607367600000.0, 18938.51, 19039.02, 18912.31, 18967.01, 1898.119135], [1607371200000.0, 18967.01, 19059.8, 18935.06, 19050.63, 1533.444692], [1607374800000.0, 19050.63, 19115.28, 19015.36, 19075.41, 1561.729683], [1607378400000.0, 19075.45, 19115.78, 19057.86, 19114.48, 788.101718], [1607382000000.0, 19114.49, 19217.64, 19072.8, 19166.9, 1603.016963], [1607385600000.0, 19166.9, 19229.99, 19132.67, 19228.4, 1539.700738], [1607389200000.0, 19228.41, 19235.6, 19150.25, 19210.62, 1465.000464], [1607392800000.0, 19210.63, 19215.34, 19160.0, 19177.61, 1061.470891], [1607396400000.0, 19177.61, 19196.0, 19132.78, 19154.73, 1086.763535], [1607400000000.0, 19154.73, 19210.0, 19151.96, 19181.21, 1423.218666], [1607403600000.0, 19181.21, 19293.76, 19181.21, 19293.76, 1362.402648], [1607407200000.0, 19293.76, 19294.84, 19091.0, 19160.49, 1931.913758], [1607410800000.0, 19160.49, 19199.62, 19135.0, 19143.1, 1877.753232], [1607414400000.0, 19143.1, 19168.88, 19010.0, 19069.96, 2558.836053], [1607418000000.0, 19069.95, 19101.59, 18700.0, 18791.77, 5751.663474], [1607421600000.0, 18791.78, 18879.0, 18700.04, 18816.0, 3764.950593], [1607425200000.0, 18816.0, 18869.23, 18730.0, 18762.96, 2445.010049], [1607428800000.0, 18762.95, 18864.06, 18610.0, 18726.93, 4536.598736], [1607432400000.0, 18726.92, 18942.3, 18726.92, 18922.84, 3398.709741], [1607436000000.0, 18923.42, 18974.83, 18861.47, 18865.01, 2255.911886], [1607439600000.0, 18865.0, 18913.14, 18780.0, 18809.91, 2017.421275], [1607443200000.0, 18809.91, 18910.0, 18745.31, 18832.03, 2390.26467], [1607446800000.0, 18832.02, 18937.81, 18818.0, 18927.41, 1365.998541], [1607450400000.0, 18927.41, 18930.84, 18826.08, 18831.0, 1791.746261], [1607454000000.0, 18831.0, 18895.0, 18787.87, 18795.74, 1381.006558], [1607457600000.0, 18795.74, 18848.0, 18664.51, 18742.63, 3177.566538], [1607461200000.0, 18742.64, 18827.34, 18687.8, 18777.86, 1763.504325], [1607464800000.0, 18778.02, 18837.45, 18320.0, 18340.01, 4197.202626], [1607468400000.0, 18336.5, 18500.0, 18200.0, 18324.11, 7082.332356], [1607472000000.0, 18324.11, 18380.0, 18120.0, 18180.01, 4284.974779], [1607475600000.0, 18180.0, 18329.65, 18032.0, 18215.49, 4099.081784], [1607479200000.0, 18215.5, 18350.36, 18151.6, 18281.0, 2790.501477], [1607482800000.0, 18281.26, 18310.0, 18220.88, 18300.01, 1888.927345], [1607486400000.0, 18300.0, 18308.39, 18125.0, 18159.58, 2057.228798], [1607490000000.0, 18159.57, 18242.32, 18058.0, 18205.5, 2983.491849], [1607493600000.0, 18206.57, 18280.0, 18153.36, 18214.09, 2142.323158], [1607497200000.0, 18214.08, 18244.26, 17830.0, 17924.07, 5288.683222], [1607500800000.0, 17924.06, 18048.12, 17650.0, 17955.78, 8243.276243], [1607504400000.0, 17955.78, 18108.64, 17919.93, 18036.34, 4587.527362], [1607508000000.0, 18040.98, 18249.0, 17978.16, 18234.65, 4049.214998], [1607511600000.0, 18234.66, 18379.87, 18180.72, 18244.95, 4005.127067], [1607515200000.0, 18244.94, 18365.0, 18210.05, 18234.74, 2551.592987], [1607518800000.0, 18234.74, 18498.0, 18169.1, 18483.5, 3956.733743], [1607522400000.0, 18483.24, 18523.99, 18425.17, 18451.01, 3858.941662], [1607526000000.0, 18451.01, 18470.22, 18290.49, 18349.5, 3458.085019], [1607529600000.0, 18349.5, 18392.0, 18252.15, 18389.74, 2491.702939], [1607533200000.0, 18389.73, 18435.0, 18297.32, 18304.06, 2386.467822], [1607536800000.0, 18304.06, 18352.22, 18165.0, 18196.4, 2456.281166], [1607540400000.0, 18196.41, 18289.06, 18196.11, 18260.03, 1802.917152], [1607544000000.0, 18260.02, 18372.0, 18211.78, 18341.02, 2640.593524], [1607547600000.0, 18341.01, 18550.0, 18326.04, 18519.37, 3203.221946], [1607551200000.0, 18519.32, 18613.99, 18485.71, 18556.11, 2659.087548], [1607554800000.0, 18556.12, 18639.57, 18531.29, 18541.28, 1699.570211], [1607558400000.0, 18541.29, 18557.32, 18416.17, 18432.01, 1824.411371], [1607562000000.0, 18432.01, 18501.96, 18303.28, 18431.25, 1984.13813], [1607565600000.0, 18431.25, 18485.71, 18353.29, 18386.02, 1684.784578], [1607569200000.0, 18386.02, 18500.0, 18350.0, 18409.77, 1492.900435], [1607572800000.0, 18409.78, 18493.28, 18389.0, 18418.15, 1491.721766], [1607576400000.0, 18418.15, 18420.8, 18305.4, 18372.37, 1571.720153], [1607580000000.0, 18372.36, 18435.74, 18278.44, 18298.22, 2065.773007], [1607583600000.0, 18298.23, 18386.16, 18287.78, 18335.91, 1225.145125], [1607587200000.0, 18335.91, 18471.18, 18319.74, 18433.3, 1918.034657], [1607590800000.0, 18433.31, 18480.0, 18224.01, 18237.52, 2921.513279], [1607594400000.0, 18237.51, 18281.01, 18070.0, 18145.0, 3256.288961], [1607598000000.0, 18145.0, 18254.48, 18117.32, 18192.49, 2447.637312], [1607601600000.0, 18192.5, 18290.63, 18165.89, 18209.96, 1783.460883], [1607605200000.0, 18209.72, 18246.8, 18045.31, 18233.65, 3702.028832], [1607608800000.0, 18233.65, 18252.55, 18040.01, 18079.99, 3043.013527], [1607612400000.0, 18080.0, 18151.0, 17911.12, 18134.08, 4777.60176], [1607616000000.0, 18132.11, 18225.31, 18096.42, 18176.99, 2836.097029], [1607619600000.0, 18176.99, 18217.95, 18076.07, 18213.86, 1822.381199], [1607623200000.0, 18213.87, 18316.76, 18195.01, 18274.75, 2327.712761], [1607626800000.0, 18274.74, 18403.28, 18225.31, 18403.1, 2437.619642], [1607630400000.0, 18403.11, 18435.74, 18342.56, 18365.78, 2400.941605], [1607634000000.0, 18365.79, 18405.62, 18305.74, 18349.0, 1405.94011], [1607637600000.0, 18348.99, 18397.77, 18297.76, 18326.36, 1007.941783], [1607641200000.0, 18326.36, 18380.41, 18220.0, 18254.63, 1461.867189], [1607644800000.0, 18254.81, 18292.73, 17950.0, 18018.32, 3824.693799], [1607648400000.0, 18018.31, 18074.8, 17804.0, 17901.45, 5573.67197], [1607652000000.0, 17901.44, 18040.81, 17801.37, 17804.97, 3702.977958], [1607655600000.0, 17804.97, 17996.14, 17715.6, 17990.88, 3471.134571], [1607659200000.0, 17990.69, 18048.6, 17929.81, 17959.72, 2129.211271], [1607662800000.0, 17959.73, 18021.42, 17833.88, 17899.44, 2208.244252], [1607666400000.0, 17899.43, 17968.0, 17814.73, 17923.6, 1844.101172], [1607670000000.0, 17923.6, 17944.87, 17700.51, 17811.95, 3651.861489], [1607673600000.0, 17811.96, 18001.15, 17810.5, 17898.79, 3799.966897], [1607677200000.0, 17899.46, 17908.0, 17728.25, 17802.6, 2599.671677], [1607680800000.0, 17802.6, 17874.01, 17572.33, 17758.45, 5006.234384], [1607684400000.0, 17758.46, 17761.1, 17600.0, 17647.71, 3575.265625], [1607688000000.0, 17647.71, 17916.5, 17617.0, 17864.73, 4028.592436], [1607691600000.0, 17864.73, 17987.98, 17827.68, 17972.01, 2900.77125], [1607695200000.0, 17972.0, 18068.0, 17901.07, 18058.76, 3657.438567], [1607698800000.0, 18058.75, 18132.0, 18028.0, 18104.74, 3191.423603], [1607702400000.0, 18104.74, 18111.55, 17942.85, 17978.14, 2468.647851], [1607706000000.0, 17978.15, 18007.0, 17852.0, 17995.53, 2771.296577], [1607709600000.0, 17995.5, 18046.83, 17943.29, 18044.26, 1971.882831], [1607713200000.0, 18044.26, 18093.95, 17959.22, 17977.0, 2055.802959], [1607716800000.0, 17975.08, 18060.0, 17925.37, 17982.89, 2012.499396], [1607720400000.0, 17983.47, 18125.18, 17952.15, 18100.01, 2363.583895], [1607724000000.0, 18100.01, 18184.0, 18067.72, 18127.81, 1977.130093], [1607727600000.0, 18127.81, 18149.75, 18012.69, 18036.53, 1824.619736], [1607731200000.0, 18036.53, 18370.0, 18020.7, 18342.06, 4583.783306], [1607734800000.0, 18342.05, 18375.0, 18271.1, 18283.84, 1922.630745], [1607738400000.0, 18283.84, 18350.0, 18278.14, 18319.99, 1370.498539], [1607742000000.0, 18319.99, 18336.82, 18268.34, 18282.02, 1178.519836], [1607745600000.0, 18282.01, 18390.0, 18261.32, 18370.28, 1520.77657], [1607749200000.0, 18370.28, 18398.19, 18310.01, 18313.36, 1600.542043], [1607752800000.0, 18313.35, 18366.24, 18278.91, 18315.76, 1342.470282], [1607756400000.0, 18315.76, 18400.0, 18300.67, 18380.85, 1514.440818], [1607760000000.0, 18380.85, 18450.0, 18318.98, 18377.64, 2153.284652], [1607763600000.0, 18377.35, 18478.0, 18345.14, 18433.47, 1957.433154], [1607767200000.0, 18433.47, 18459.42, 18370.0, 18382.1, 1482.558626], [1607770800000.0, 18382.11, 18513.66, 18366.68, 18506.1, 1892.703046], [1607774400000.0, 18506.1, 18525.33, 18427.01, 18445.15, 1831.731773], [1607778000000.0, 18445.14, 18451.35, 18388.88, 18400.21, 1443.035834], [1607781600000.0, 18400.21, 18475.63, 18308.82, 18372.97, 2537.663833], [1607785200000.0, 18372.98, 18434.62, 18317.67, 18399.01, 1786.916682], [1607788800000.0, 18399.01, 18450.46, 18362.42, 18397.88, 1754.689938], [1607792400000.0, 18397.87, 18527.93, 18374.07, 18483.17, 1642.530837], [1607796000000.0, 18482.83, 18746.33, 18482.47, 18687.62, 4917.028007], [1607799600000.0, 18687.63, 18850.0, 18687.63, 18805.29, 3255.899793], [1607803200000.0, 18805.29, 18840.4, 18745.01, 18770.68, 2031.985016], [1607806800000.0, 18770.67, 18840.4, 18729.09, 18787.72, 1855.293184], [1607810400000.0, 18787.72, 18948.66, 18754.37, 18870.51, 2213.09286], [1607814000000.0, 18872.07, 18880.37, 18768.77, 18808.69, 1730.469058], [1607817600000.0, 18808.69, 18875.0, 18711.12, 18750.0, 2078.489661], [1607821200000.0, 18750.0, 18808.98, 18711.57, 18795.31, 1142.36527], [1607824800000.0, 18795.12, 18830.0, 18750.02, 18812.61, 1530.614138], [1607828400000.0, 18812.61, 18831.36, 18760.77, 18784.42, 932.07127], [1607832000000.0, 18784.56, 18884.5, 18768.19, 18852.51, 1252.673306], [1607835600000.0, 18852.51, 18938.55, 18815.0, 18850.0, 2197.616883], [1607839200000.0, 18849.99, 18984.55, 18844.95, 18973.93, 1930.019028], [1607842800000.0, 18973.93, 19306.27, 18973.93, 19247.68, 7062.527311], [1607846400000.0, 19249.21, 19322.24, 19179.5, 19245.91, 3622.141403], [1607850000000.0, 19245.25, 19301.14, 19185.74, 19263.25, 2399.866077], [1607853600000.0, 19263.26, 19370.0, 19255.0, 19278.99, 3202.08969], [1607857200000.0, 19278.99, 19349.0, 19200.0, 19316.81, 2247.859112], [1607860800000.0, 19316.8, 19400.0, 19265.91, 19348.97, 3516.700147], [1607864400000.0, 19348.97, 19411.0, 19290.01, 19321.13, 2820.186035], [1607868000000.0, 19321.13, 19364.0, 19275.41, 19292.13, 1662.41871], [1607871600000.0, 19292.13, 19334.08, 19080.0, 19211.34, 3876.890837], [1607875200000.0, 19211.34, 19385.0, 19208.7, 19328.12, 3401.557937], [1607878800000.0, 19328.11, 19334.85, 19197.36, 19273.6, 1835.01721], [1607882400000.0, 19273.59, 19273.6, 19172.33, 19185.33, 1636.424148], [1607886000000.0, 19185.33, 19233.0, 19155.0, 19193.78, 1307.778726], [1607889600000.0, 19193.28, 19193.29, 19089.63, 19154.57, 1908.19497], [1607893200000.0, 19154.56, 19225.63, 19150.0, 19190.01, 1153.642827], [1607896800000.0, 19190.0, 19194.33, 18971.0, 19112.41, 2215.95021], [1607900400000.0, 19112.41, 19225.0, 19090.0, 19174.99, 1627.726838], [1607904000000.0, 19174.99, 19174.99, 19000.0, 19057.19, 1888.751084], [1607907600000.0, 19056.23, 19112.74, 19019.65, 19085.64, 1246.655384], [1607911200000.0, 19085.64, 19307.09, 19052.62, 19297.7, 2765.118326], [1607914800000.0, 19297.9, 19347.0, 19227.01, 19260.68, 2413.537572], [1607918400000.0, 19260.68, 19282.11, 19055.51, 19126.94, 2675.257429], [1607922000000.0, 19126.93, 19130.0, 19033.84, 19085.53, 1556.626987], [1607925600000.0, 19085.53, 19207.93, 19080.4, 19206.23, 1741.878147], [1607929200000.0, 19206.24, 19210.0, 19100.43, 19155.8, 1326.684257], [1607932800000.0, 19155.8, 19244.69, 19140.0, 19222.0, 2065.456886], [1607936400000.0, 19222.0, 19258.96, 19120.0, 19184.61, 1986.246842], [1607940000000.0, 19184.6, 19215.19, 19052.72, 19088.94, 2101.350737], [1607943600000.0, 19088.94, 19144.14, 19051.11, 19106.44, 1864.319235], [1607947200000.0, 19106.43, 19147.0, 19028.97, 19114.66, 2118.835216], [1607950800000.0, 19114.66, 19199.86, 19063.31, 19184.19, 2220.236947], [1607954400000.0, 19184.2, 19227.66, 19088.88, 19221.74, 2439.250888], [1607958000000.0, 19221.75, 19300.0, 19158.57, 19200.07, 3589.350164], [1607961600000.0, 19200.0, 19237.79, 19114.58, 19152.37, 1896.167202], [1607965200000.0, 19152.59, 19170.51, 19113.08, 19143.09, 1255.884608], [1607968800000.0, 19143.38, 19236.76, 19143.38, 19236.75, 1817.186019], [1607972400000.0, 19236.75, 19236.76, 19165.0, 19175.88, 1343.180358], [1607976000000.0, 19175.88, 19216.57, 19130.85, 19208.07, 1386.346738], [1607979600000.0, 19208.08, 19220.85, 19150.85, 19197.83, 1359.138901], [1607983200000.0, 19197.82, 19296.0, 19188.76, 19291.9, 1317.369348], [1607986800000.0, 19291.99, 19349.0, 19200.85, 19273.14, 2882.372019], [1607990400000.0, 19273.69, 19395.0, 19243.64, 19394.94, 2998.531387], [1607994000000.0, 19394.94, 19470.0, 19320.85, 19455.06, 2941.282431], [1607997600000.0, 19455.06, 19570.0, 19444.25, 19458.97, 5055.6835], [1608001200000.0, 19458.98, 19509.57, 19416.35, 19479.7, 1934.600997], [1608004800000.0, 19479.69, 19497.36, 19161.03, 19180.71, 3179.726241], [1608008400000.0, 19180.7, 19259.78, 19050.0, 19132.4, 3984.043664], [1608012000000.0, 19133.29, 19211.76, 19101.0, 19186.12, 2257.168193], [1608015600000.0, 19186.12, 19226.95, 19146.95, 19197.6, 1730.803117], [1608019200000.0, 19197.59, 19219.96, 19106.95, 19137.24, 1854.09426], [1608022800000.0, 19136.96, 19209.99, 19074.0, 19187.51, 2498.768], [1608026400000.0, 19187.51, 19350.0, 19117.9, 19310.0, 3596.157208], [1608030000000.0, 19309.99, 19334.9, 19241.07, 19293.54, 1837.391665], [1608033600000.0, 19293.54, 19383.98, 19256.33, 19291.4, 2177.678422], [1608037200000.0, 19291.4, 19349.0, 19263.5, 19349.0, 1506.492391], [1608040800000.0, 19349.16, 19433.0, 19256.27, 19337.46, 3320.865118], [1608044400000.0, 19337.46, 19429.27, 19337.46, 19403.31, 2771.86623], [1608048000000.0, 19403.31, 19425.87, 19328.06, 19364.1, 2346.463058], [1608051600000.0, 19364.09, 19422.92, 19352.48, 19399.53, 1851.189273], [1608055200000.0, 19399.53, 19543.0, 19390.0, 19530.0, 3565.309308], [1608058800000.0, 19529.99, 19545.0, 19465.0, 19530.38, 2470.61552], [1608062400000.0, 19530.38, 19547.0, 19461.55, 19491.87, 1595.053706], [1608066000000.0, 19491.87, 19511.99, 19276.0, 19419.92, 3770.651415], [1608069600000.0, 19419.93, 19489.09, 19379.19, 19461.38, 1176.976643], [1608073200000.0, 19461.37, 19471.18, 19345.51, 19426.43, 1412.954264], [1608076800000.0, 19426.43, 19454.97, 19278.6, 19365.29, 2363.836441], [1608080400000.0, 19365.28, 19420.0, 19317.01, 19389.37, 1791.407177], [1608084000000.0, 19389.37, 19488.02, 19389.37, 19442.08, 1919.597241], [1608087600000.0, 19442.08, 19454.0, 19325.0, 19346.52, 2010.880432], [1608091200000.0, 19346.51, 19403.07, 19300.3, 19358.68, 1417.796057], [1608094800000.0, 19358.31, 19421.8, 19339.35, 19373.81, 1444.892753], [1608098400000.0, 19373.82, 19454.93, 19341.4, 19429.89, 1512.570737], [1608102000000.0, 19429.9, 19451.03, 19399.0, 19423.96, 1290.055946], [1608105600000.0, 19423.95, 19487.17, 19370.62, 19482.57, 1854.4269], [1608109200000.0, 19482.57, 19525.0, 19420.84, 19516.69, 2118.952672], [1608112800000.0, 19516.22, 19800.0, 19498.01, 19798.17, 8207.25114], [1608116400000.0, 19798.18, 19860.0, 19645.9, 19739.78, 5884.326771], [1608120000000.0, 19739.78, 19889.99, 19680.0, 19762.81, 6075.101618], [1608123600000.0, 19762.8, 20450.0, 19762.8, 20319.51, 11510.059772], [1608127200000.0, 20320.85, 20799.0, 20206.16, 20649.0, 14801.122043], [1608130800000.0, 20650.01, 20733.0, 20539.0, 20661.37, 7170.614379], [1608134400000.0, 20661.37, 20865.43, 20620.0, 20854.56, 7391.879642], [1608138000000.0, 20854.56, 20855.0, 20573.82, 20639.82, 5243.123474], [1608141600000.0, 20639.82, 20737.44, 20550.0, 20585.79, 3487.839577], [1608145200000.0, 20585.79, 20766.39, 20550.0, 20736.87, 2693.660582], [1608148800000.0, 20736.87, 20839.0, 20727.3, 20802.82, 3210.854218], [1608152400000.0, 20802.82, 21288.0, 20711.0, 21192.78, 7677.808354], [1608156000000.0, 21191.53, 21444.44, 21172.79, 21366.42, 6275.220393], [1608159600000.0, 21366.02, 21560.0, 21200.0, 21335.52, 6953.057251], [1608163200000.0, 21335.52, 21400.0, 21230.0, 21389.25, 4427.884694], [1608166800000.0, 21389.26, 21860.05, 21389.26, 21719.22, 6860.927779], [1608170400000.0, 21719.77, 21994.0, 21642.13, 21913.9, 5864.760533], [1608174000000.0, 21913.91, 22166.0, 21703.67, 21753.26, 7372.388063], [1608177600000.0, 21752.65, 21900.0, 21735.09, 21785.88, 3477.941858], [1608181200000.0, 21785.87, 22311.38, 21781.99, 22280.0, 5326.441496], [1608184800000.0, 22280.0, 22400.0, 22053.0, 22172.72, 6338.231327], [1608188400000.0, 22172.72, 22488.0, 22102.0, 22478.76, 5356.558094], [1608192000000.0, 22478.75, 22990.0, 22400.0, 22904.7, 12107.706886], [1608195600000.0, 22904.7, 23800.0, 21801.0, 22650.0, 23832.91859], [1608199200000.0, 22648.86, 22934.0, 22380.79, 22618.11, 11643.529767], [1608202800000.0, 22617.73, 22808.56, 22528.73, 22752.16, 5168.285261], [1608206400000.0, 22752.15, 23199.0, 22600.0, 23149.99, 6628.225882], [1608210000000.0, 23150.0, 23348.0, 22647.51, 22831.84, 7791.389716], [1608213600000.0, 22832.73, 23257.9, 22715.38, 23086.01, 7463.214961], [1608217200000.0, 23086.0, 23369.0, 22900.0, 23333.8, 6953.463504], [1608220800000.0, 23333.81, 23650.0, 23200.0, 23591.23, 10032.336464], [1608224400000.0, 23592.2, 23699.7, 23000.0, 23023.98, 6924.834078], [1608228000000.0, 23023.98, 23280.1, 22500.0, 23250.27, 11618.263635], [1608231600000.0, 23251.16, 23497.0, 22804.22, 22899.08, 7447.203472], [1608235200000.0, 22898.47, 23106.34, 22311.1, 22791.55, 8469.168153], [1608238800000.0, 22791.78, 22848.39, 22382.7, 22791.96, 5412.104163], [1608242400000.0, 22785.93, 23080.0, 22757.52, 22963.04, 3385.2089], [1608246000000.0, 22963.05, 23000.0, 22570.59, 22797.16, 4979.489472], [1608249600000.0, 22797.15, 22842.76, 22470.35, 22764.77, 3923.98594], [1608253200000.0, 22764.77, 23146.95, 22634.11, 22988.21, 4155.082395], [1608256800000.0, 22988.21, 23248.99, 22937.9, 23003.31, 3844.945472], [1608260400000.0, 23003.31, 23047.88, 22762.05, 22811.59, 3180.188409], [1608264000000.0, 22811.58, 22973.92, 22772.05, 22870.93, 2602.419348], [1608267600000.0, 22870.92, 23028.13, 22835.02, 22955.51, 2184.781421], [1608271200000.0, 22955.51, 23168.59, 22874.69, 22994.49, 3086.571333], [1608274800000.0, 22992.06, 23069.44, 22838.19, 23055.98, 3071.638245], [1608278400000.0, 23055.98, 23285.18, 22938.87, 23114.84, 4699.90906], [1608282000000.0, 23114.84, 23215.0, 23017.97, 23212.82, 2878.472119], [1608285600000.0, 23212.82, 23220.0, 22933.07, 22964.67, 3730.390769], [1608289200000.0, 22964.12, 22967.29, 22691.61, 22886.17, 4048.00235], [1608292800000.0, 22886.18, 23073.25, 22727.0, 22939.42, 3427.454017], [1608296400000.0, 22939.41, 22948.64, 22548.99, 22571.78, 4037.637495], [1608300000000.0, 22571.79, 22758.44, 22400.0, 22610.65, 5664.078883], [1608303600000.0, 22610.65, 22636.27, 22350.0, 22549.0, 4804.643291], [1608307200000.0, 22549.0, 22752.77, 22463.59, 22719.28, 3562.521096], [1608310800000.0, 22719.29, 22818.0, 22670.22, 22749.32, 2476.345002], [1608314400000.0, 22749.32, 22829.04, 22694.22, 22781.44, 2092.86434], [1608318000000.0, 22781.44, 22783.42, 22626.81, 22762.28, 1880.94478], [1608321600000.0, 22762.28, 22795.11, 22650.29, 22755.66, 1760.425011], [1608325200000.0, 22755.25, 23078.47, 22751.1, 22880.07, 3998.59848], [1608328800000.0, 22880.07, 23034.33, 22800.0, 23011.38, 1992.223703], [1608332400000.0, 23011.38, 23143.56, 22908.45, 23107.39, 2542.011356], [1608336000000.0, 23107.39, 23168.28, 22940.0, 22954.02, 2050.965871], [1608339600000.0, 22954.02, 23099.0, 22902.1, 23046.76, 2149.001638], [1608343200000.0, 23046.75, 23220.0, 23034.75, 23220.0, 2910.223586], [1608346800000.0, 23219.51, 23225.0, 23093.93, 23098.04, 2329.278504], [1608350400000.0, 23098.05, 23138.0, 23032.0, 23043.4, 1859.427393], [1608354000000.0, 23043.41, 23075.36, 22924.79, 22958.0, 2023.000091], [1608357600000.0, 22958.48, 23010.6, 22821.0, 22853.5, 1813.015202], [1608361200000.0, 22853.51, 23038.0, 22832.0, 22853.75, 1791.212957], [1608364800000.0, 22853.75, 22990.0, 22750.0, 22983.77, 3048.665524], [1608368400000.0, 22983.77, 23045.49, 22928.05, 22973.06, 2229.925469], [1608372000000.0, 22973.06, 23080.45, 22950.0, 23019.99, 2331.997056], [1608375600000.0, 23020.0, 23063.49, 22875.01, 22888.54, 1942.204736], [1608379200000.0, 22888.53, 23127.72, 22886.79, 23041.53, 3218.568336], [1608382800000.0, 23041.53, 23189.1, 22992.23, 23172.74, 3234.586672], [1608386400000.0, 23172.75, 23627.99, 23052.0, 23296.96, 10267.116714], [1608390000000.0, 23296.96, 23650.0, 23296.95, 23552.01, 6178.384265], [1608393600000.0, 23552.0, 24171.47, 23456.55, 23966.48, 13881.907694], [1608397200000.0, 23966.48, 24100.0, 23680.0, 23886.44, 6294.413079], [1608400800000.0, 23886.71, 23906.66, 23556.76, 23822.66, 4060.081676], [1608404400000.0, 23822.66, 23883.79, 23651.0, 23791.91, 3011.088133], [1608408000000.0, 23791.82, 23937.0, 23782.91, 23902.17, 2634.921207], [1608411600000.0, 23902.19, 24065.41, 23780.21, 23974.71, 3253.897865], [1608415200000.0, 23974.7, 23999.0, 23825.95, 23905.73, 1543.403326], [1608418800000.0, 23905.73, 23915.26, 23719.25, 23821.61, 1987.777683], [1608422400000.0, 23821.6, 23836.48, 23230.0, 23481.41, 5981.312918], [1608426000000.0, 23483.11, 23548.72, 23390.0, 23485.56, 2118.503927], [1608429600000.0, 23486.42, 23542.99, 23300.0, 23429.92, 2007.609104], [1608433200000.0, 23429.92, 23429.92, 23180.88, 23346.48, 2578.315434], [1608436800000.0, 23346.25, 23452.63, 23060.0, 23426.54, 2789.303747], [1608440400000.0, 23426.15, 23588.88, 23397.58, 23481.38, 2113.937874], [1608444000000.0, 23481.38, 23614.84, 23459.98, 23506.67, 1518.982809], [1608447600000.0, 23506.67, 23646.31, 23410.62, 23628.88, 2081.734038], [1608451200000.0, 23628.89, 23791.0, 23532.0, 23698.49, 2953.113346], [1608454800000.0, 23698.49, 23748.4, 23503.0, 23592.92, 2928.141217], [1608458400000.0, 23592.93, 23648.92, 23358.78, 23394.76, 3238.112754], [1608462000000.0, 23394.77, 23625.0, 23393.0, 23553.02, 1945.517865], [1608465600000.0, 23553.02, 23588.71, 23333.57, 23472.44, 2466.127241], [1608469200000.0, 23472.45, 23590.9, 23296.0, 23561.36, 3044.961002], [1608472800000.0, 23561.36, 23682.0, 23500.19, 23537.7, 2538.180355], [1608476400000.0, 23537.7, 23910.0, 23527.48, 23868.09, 4012.70827], [1608480000000.0, 23868.08, 23901.01, 23628.31, 23630.1, 3492.542838], [1608483600000.0, 23630.1, 23800.0, 23625.41, 23722.62, 2050.917554], [1608487200000.0, 23722.62, 23877.0, 23655.26, 23866.69, 2433.334581], [1608490800000.0, 23866.68, 23995.97, 23780.83, 23930.0, 2727.20596], [1608494400000.0, 23928.8, 24295.0, 23850.18, 24172.25, 6396.944648], [1608498000000.0, 24172.99, 24208.62, 23350.0, 23373.05, 7510.998452], [1608501600000.0, 23376.94, 23614.36, 23090.0, 23507.79, 5563.847281], [1608505200000.0, 23507.03, 23594.73, 23450.0, 23455.52, 2197.79247], [1608508800000.0, 23455.54, 23709.31, 23287.94, 23679.55, 3148.360382], [1608512400000.0, 23679.55, 23744.86, 23587.85, 23663.48, 1823.646184], [1608516000000.0, 23663.49, 23887.0, 23652.48, 23856.38, 2136.222374], [1608519600000.0, 23856.39, 24016.93, 23657.5, 23945.29, 3842.134228], [1608523200000.0, 23945.3, 24102.77, 23790.0, 23895.73, 3537.794056], [1608526800000.0, 23895.02, 23975.0, 23841.99, 23909.83, 2315.212127], [1608530400000.0, 23909.83, 23926.8, 23700.0, 23921.73, 2593.611244], [1608534000000.0, 23921.74, 24028.15, 23888.15, 23980.0, 3110.573389], [1608537600000.0, 23979.99, 24075.94, 23635.08, 23659.59, 4670.537852], [1608541200000.0, 23659.59, 23739.18, 23328.0, 23461.35, 6435.521197], [1608544800000.0, 23460.93, 23495.91, 22441.01, 22445.99, 13511.910357], [1608548400000.0, 22446.39, 22833.01, 22350.0, 22645.85, 7582.69498], [1608552000000.0, 22646.7, 22663.0, 21815.0, 22307.5, 11239.201922], [1608555600000.0, 22307.5, 22665.35, 22251.23, 22646.53, 2830.345587], [1608559200000.0, 22646.53, 22646.53, 22646.53, 22646.53, 0.0], [1608573600000.0, 22693.65, 22988.6, 22621.44, 22813.66, 4796.306546], [1608577200000.0, 22813.65, 22940.3, 22681.32, 22816.62, 2874.578236], [1608580800000.0, 22816.63, 22930.0, 22732.78, 22829.79, 1900.463944], [1608584400000.0, 22829.79, 23162.26, 22765.0, 23127.37, 3028.124973], [1608588000000.0, 23127.38, 23254.33, 23021.17, 23170.89, 2084.951696], [1608591600000.0, 23169.88, 23228.35, 22699.99, 22719.71, 3712.997151], [1608595200000.0, 22719.88, 22926.14, 22500.0, 22558.42, 4435.40638], [1608598800000.0, 22558.41, 22875.61, 22428.86, 22753.99, 3850.003384], [1608602400000.0, 22753.99, 22970.0, 22730.0, 22957.57, 2524.453641], [1608606000000.0, 22957.58, 22969.0, 22737.9, 22851.2, 2438.400079], [1608609600000.0, 22851.19, 23076.77, 22851.19, 22951.3, 2681.871362], [1608613200000.0, 22951.3, 22970.0, 22610.19, 22681.51, 3600.425979], [1608616800000.0, 22681.5, 22870.02, 22551.02, 22750.96, 3044.631907], [1608620400000.0, 22750.85, 22826.78, 22500.0, 22655.83, 3555.118357], [1608624000000.0, 22655.82, 22750.0, 22353.4, 22689.86, 4786.238036], [1608627600000.0, 22689.87, 22822.83, 22600.0, 22783.85, 3441.122021], [1608631200000.0, 22782.92, 22819.76, 22602.12, 22701.2, 2835.326798], [1608634800000.0, 22701.2, 23155.0, 22701.2, 23119.48, 4983.540952], [1608638400000.0, 23119.47, 23300.0, 23062.01, 23175.57, 4777.70466], [1608642000000.0, 23175.57, 23536.96, 23103.98, 23487.2, 4972.84068], [1608645600000.0, 23487.2, 23628.89, 23335.83, 23439.99, 6009.583132], [1608649200000.0, 23439.99, 23600.0, 23300.42, 23342.58, 4452.75789], [1608652800000.0, 23342.68, 23456.0, 23237.0, 23348.95, 4298.902555], [1608656400000.0, 23348.43, 23442.0, 23224.4, 23342.54, 2756.859302], [1608660000000.0, 23342.54, 23520.0, 23342.51, 23435.27, 2784.592049]]\n    }\n\n\n@pytest.fixture\ndef default_trades_data():\n    # imported from real backtesting data\n    return {\n        \"BTC/USDT\": [\n            {commons_enums.PlotAttributes.X.value: 1607986800000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00617086,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\",\n             commons_enums.PlotAttributes.Y.value: 19291.9,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.BUY.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 6.17e-06,\n             commons_enums.DBRows.FEES_CURRENCY.value: 'BTC'},\n            {commons_enums.PlotAttributes.X.value: 1608040800000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00614347,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\",\n             commons_enums.PlotAttributes.Y.value: 19349.0,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.BUY.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 6.14e-06,\n             commons_enums.DBRows.FEES_CURRENCY.value: 'BTC'},\n            {commons_enums.PlotAttributes.X.value: 1608134400000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00616469,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\",\n             commons_enums.PlotAttributes.Y.value: 20835.252,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 0.12834515,\n             commons_enums.DBRows.FEES_CURRENCY.value: 'USDT'},\n            {commons_enums.PlotAttributes.X.value: 1608152400000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00613733,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\",\n             commons_enums.PlotAttributes.Y.value: 20896.92,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 0.12830709,\n             commons_enums.DBRows.FEES_CURRENCY.value: 'USDT'},\n            {commons_enums.PlotAttributes.X.value: 1608343200000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00526114,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\",\n             commons_enums.PlotAttributes.Y.value: 23046.76,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.BUY.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 5.26e-06,\n             commons_enums.DBRows.FEES_CURRENCY.value: 'BTC'},\n            {commons_enums.PlotAttributes.X.value: 1608390000000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.0051656,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\",\n             commons_enums.PlotAttributes.Y.value: 23296.96,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.BUY.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 5.17e-06,\n             commons_enums.DBRows.FEES_CURRENCY.value: 'BTC'},\n            {commons_enums.PlotAttributes.X.value: 1608548400000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00516043,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\", commons_enums.PlotAttributes.Y.value: 22365.0816,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 0.11540382, commons_enums.DBRows.FEES_CURRENCY.value: 'USDT'},\n            {commons_enums.PlotAttributes.X.value: 1608552000000,\n             commons_enums.PlotAttributes.VOLUME.value: 0.00525588,\n             commons_enums.DBRows.SYMBOL.value: \"BTC/USDT\", commons_enums.PlotAttributes.Y.value: 22124.8896,\n             commons_enums.PlotAttributes.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n             commons_enums.DBRows.FEES_AMOUNT.value: 0.11637692, commons_enums.DBRows.FEES_CURRENCY.value: 'USDT'},\n\n        ]\n    }\n\n\n@pytest.fixture\ndef default_portfolio_historical_value():\n    # imported from real backtesting data, verified values\n    return [1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 999.8815237991, 999.7687099720999, 1000.5161786346, 1000.8867997973999, 1000.9109653822, 1001.0386361121, 999.195455449, 998.9031874960999, 999.2288680688, 999.2995770631, 998.9258119084, 999.2374369879, 999.9924882191, 999.8910790686, 999.877886632, 1000.1161382391999, 999.9722046052, 1000.7822926222, 1000.2998073977999, 1000.7357909865999, 1002.3407125158, 1002.3455103035999, 1001.8717595133999, 1000.9867521946, 1001.4965479033999, 1001.0667153246, 1000.3144468016, 1000.6108024634, 1001.2592419376, 1000.0835378861999, 1000.2287017222, 1000.4195060523999, 1001.1094033339999, 1001.036206315, 1001.7573507274, 1002.1713137003999, 1005.6399912595999, 1004.9215532915999, 1005.2047457919999, 1012.0698880529999, 1016.1192209561999, 1017.2025553799799, 1018.38822616268, 1017.0702959184799, 1016.7386959785799, 1017.6659237949799, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.5198963714798, 1018.3986178550799, 1019.3066236838799, 1018.6682444990798, 1018.3810632158799, 1017.9346813274799, 1017.3829716038799, 1017.3842330150799, 1018.0676025326799, 1018.0113120578799, 1018.2580230650799, 1017.5670325214799, 1018.3711821614799, 1019.0608587350798, 1019.5932463066798, 1022.2498220090798, 1026.56717417788, 1025.7362651291799, 1025.0691004736798, 1024.74786147328, 1025.8975096079798, 1026.6527962460798, 1025.93438334538, 1025.0580591850799, 1021.5322424131798, 1021.5667203992798, 1020.9781988842799, 1020.1066662265798, 1020.9389293955799, 1021.5142221968799, 1021.7776506767798, 1023.0507320849798, 1023.7757072609799, 1022.6761615773798, 1020.6120655877799, 1022.2604466452799, 1021.4212045485799, 1022.3473186706799, 1022.1008687760799, 1025.54220927388, 1023.0633358200798, 1024.0270528212798, 1025.52762643988, 1026.1746876170798, 1028.7182463559798, 1020.4263427804799, 1021.7814005483799, 1021.2450647464799, 1023.5784223495798, 1023.4111364109799, 1025.4204426099798, 1026.3465567320798, 1025.8228246652798, 1025.9770902163798, 1026.1011484684798, 1026.7078985259798, 1023.3705128019799, 1021.3012086573799, 1010.1984553833678, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158, 1008.3923109410158]\n\n\n@pytest.fixture\ndef default_portfolio_data():\n    return {'BTC': 0.0, 'USDT': 1000.0}\n\n\n@pytest.fixture\ndef default_spot_metadata():\n    return {\n        commons_enums.DBRows.EXCHANGES.value: \"binance\",\n        commons_enums.DBRows.FUTURE_CONTRACTS.value: {},\n    }\n\n\n@pytest.fixture\ndef default_pnl_historical_value():\n    # imported from real backtesting data, verified values\n    # add 0 at the end for the end backtesting value\n    return [0, 9.266910467879995, 9.25298590360002, -3.7521819056716623, -6.375403524792318, 0]\n\n\n@pytest.fixture\ndef default_funding_fees_data():\n    #TODO\n    return []\n\n\n@pytest.fixture\ndef default_realized_pnl_history():\n    #TODO\n    return []\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/backtesting/test_backtesting_data_collector.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport octobot_commons.enums as common_enums\nimport octobot_commons.constants as common_constants\n\nimport octobot.constants as constants\n\nimport tentacles.Meta.Keywords.scripting_library.backtesting.backtesting_data_collector as src_backtesting_data_collector\nimport tentacles.Meta.Keywords.scripting_library.errors as errors\n\nclass DummyLogger:\n    def __init__(self):\n        self.infos = []\n        self.errors = []\n        self.exceptions = []\n    def info(self, msg):\n        self.infos.append(msg)\n    def error(self, msg):\n        self.errors.append(msg)\n    def exception(self, err, *args, **kwargs):\n        self.exceptions.append((err, args, kwargs))\n\ndef patch_logger(monkeypatch):\n    logger = DummyLogger()\n    monkeypatch.setattr(src_backtesting_data_collector, \"_get_logger\", lambda: logger)\n    return logger\n\ndef base_args():\n    return dict(\n        exchange=\"binance\",\n        symbol=\"BTC/USDT\",\n        time_frame=common_enums.TimeFrames.ONE_HOUR,\n        allow_candles_beyond_range=False,\n        required_from_the_start=True,\n        required_till_the_end=True,\n        first_traded_symbols_time=9999999999,  # large for test\n        allow_any_backtesting_start_and_end_time=False,\n    )\n\n\ndef test_ensure_compatible_candle_time_normal_case(monkeypatch):\n    logger = patch_logger(monkeypatch)\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 10 * tf_sec\n    first_candle_time = first_open_time\n    last_candle_time = last_open_time\n    result = src_backtesting_data_collector.ensure_compatible_candle_time(\n        **args,\n        first_open_time=first_open_time,\n        last_open_time=last_open_time,\n        first_candle_time=first_candle_time,\n        last_candle_time=last_candle_time,\n    )\n    assert result is None\n    assert not logger.errors\n    assert not logger.infos\n\ndef test_ensure_compatible_candle_time_starts_too_early():\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 10 * tf_sec\n    first_candle_time = first_open_time - constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW - 1\n    last_candle_time = last_open_time\n    with pytest.raises(errors.InvalidBacktestingDataError) as exc:\n        src_backtesting_data_collector.ensure_compatible_candle_time(\n            **args,\n            first_open_time=first_open_time,\n            last_open_time=last_open_time,\n            first_candle_time=first_candle_time,\n            last_candle_time=last_candle_time,\n        )\n    assert \"starts too early\" in str(exc.value)\n\ndef test_ensure_compatible_candle_time_starts_too_late_and_required():\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 10 * tf_sec\n    first_candle_time = first_open_time + tf_sec * 2\n    last_candle_time = last_open_time\n    args[\"first_traded_symbols_time\"] = first_open_time - constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW  # force fail\n    with pytest.raises(errors.InvalidBacktestingDataError) as exc:\n        src_backtesting_data_collector.ensure_compatible_candle_time(\n            **args,\n            first_open_time=first_open_time,\n            last_open_time=last_open_time,\n            first_candle_time=first_candle_time,\n            last_candle_time=last_candle_time,\n        )\n    assert \"starts too late\" in str(exc.value)\n\ndef test_ensure_compatible_candle_time_starts_too_late_but_adapted_with_test_data(monkeypatch):\n    logger = patch_logger(monkeypatch)\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 10 * tf_sec\n    first_candle_time = first_open_time + tf_sec * 2\n    last_candle_time = last_open_time\n    args[\"first_traded_symbols_time\"] = first_open_time + tf_sec * 3  # allow adaptation\n    result = src_backtesting_data_collector.ensure_compatible_candle_time(\n        **args,\n        first_open_time=first_open_time,\n        last_open_time=last_open_time,\n        first_candle_time=first_candle_time,\n        last_candle_time=last_candle_time,\n    )\n    assert result == first_candle_time\n    assert any(\"acceptable, start time is adapted\" in msg for msg in logger.infos)\n\ndef test_ensure_compatible_candle_time_starts_too_late_but_adapted_with_real_data_dca(monkeypatch):\n    logger = patch_logger(monkeypatch)\n    args = base_args()\n    args[\"time_frame\"] = common_enums.TimeFrames.FOUR_HOURS\n    first_open_time = 1737424774.2265518 # Tuesday, January 21, 2025 9:44:54.459\n    last_open_time = 1752990294.4590356 # Sunday, July 20, 2025 5:44:54.459\n    first_candle_time = 1737446400  # Tuesday, January 21, 2025 12:00:00\n    last_candle_time = 1752955200 # Saturday, July 19, 2025 20:00:00\n    args[\"first_traded_symbols_time\"] = 1737465882.5380511  # Tuesday, January 21, 2025 13:24:42.538\n    # fails without the kw_constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW allowance over first_traded_symbols_time\n    result = src_backtesting_data_collector.ensure_compatible_candle_time(\n        **args,\n        first_open_time=first_open_time,\n        last_open_time=last_open_time,\n        first_candle_time=first_candle_time,\n        last_candle_time=last_candle_time,\n    )\n    assert result == first_candle_time\n    assert any(\"acceptable, start time is adapted\" in msg for msg in logger.infos)\n    \n    first_candle_time = args[\"first_traded_symbols_time\"] + constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW  # Thursday, January 23, 2025 13:24:42.538\n    with pytest.raises(errors.InvalidBacktestingDataError) as exc:\n      result = src_backtesting_data_collector.ensure_compatible_candle_time(\n          **args,\n          first_open_time=first_open_time,\n          last_open_time=last_open_time,\n          first_candle_time=first_candle_time,\n          last_candle_time=last_candle_time,\n      )\n    assert \"starts too late\" in str(exc.value)\n\ndef test_ensure_compatible_candle_time_starts_too_late_but_adapted_with_real_data_basked(monkeypatch):\n    logger = patch_logger(monkeypatch)\n    args = base_args()\n    args[\"time_frame\"] = common_enums.TimeFrames.FOUR_HOURS\n    first_open_time = 1737453626.6562696 # Tuesday, January 21, 2025 10:00:26.656\n    last_open_time = 1752919226.658268 # Saturday, July 19, 2025 10:00:26.658\n    first_candle_time = 1737590400  # Thursday, January 23, 2025 0:00:00\n    last_candle_time = 1752883200 # Saturday, July 19, 2025 0:00:00\n    args[\"first_traded_symbols_time\"] = 1749325565.048149  # Saturday, June 7, 2025 19:46:05.048\n    result = src_backtesting_data_collector.ensure_compatible_candle_time(\n        **args,\n        first_open_time=first_open_time,\n        last_open_time=last_open_time,\n        first_candle_time=first_candle_time,\n        last_candle_time=last_candle_time,\n    )\n    assert result == first_candle_time\n    assert any(\"acceptable, start time is adapted\" in msg for msg in logger.infos)\n\ndef test_ensure_compatible_candle_time_ends_too_late():\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 10 * tf_sec\n    first_candle_time = first_open_time\n    last_candle_time = last_open_time + tf_sec * 2\n    with pytest.raises(errors.InvalidBacktestingDataError) as exc:\n        src_backtesting_data_collector.ensure_compatible_candle_time(\n            **args,\n            first_open_time=first_open_time,\n            last_open_time=last_open_time,\n            first_candle_time=first_candle_time,\n            last_candle_time=last_candle_time,\n        )\n    assert \"ends too late\" in str(exc.value)\n\ndef test_ensure_compatible_candle_time_ends_too_early_and_required():\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 10 * tf_sec\n    first_candle_time = first_open_time\n    last_candle_time = last_open_time - constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW - 1\n    with pytest.raises(errors.InvalidBacktestingDataError) as exc:\n        src_backtesting_data_collector.ensure_compatible_candle_time(\n            **args,\n            first_open_time=first_open_time,\n            last_open_time=last_open_time,\n            first_candle_time=first_candle_time,\n            last_candle_time=last_candle_time,\n        )\n    assert \"ends too early\" in str(exc.value)\n\ndef test_ensure_compatible_candle_time_ends_too_early_but_not_required(monkeypatch):\n    logger = patch_logger(monkeypatch)\n    args = base_args()\n    args[\"required_till_the_end\"] = False\n    first_open_time = 1000000\n    last_open_time = 1000000 + constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW\n    first_candle_time = first_open_time\n    last_candle_time = last_open_time - constants.BACKTESTING_DATA_ALLOWED_PRICE_WINDOW  - 1\n    result = src_backtesting_data_collector.ensure_compatible_candle_time(\n        **args,\n        first_open_time=first_open_time,\n        last_open_time=last_open_time,\n        first_candle_time=first_candle_time,\n        last_candle_time=last_candle_time,\n    )\n    assert result is None\n    assert any(\"acceptable, this symbol is not required till the end\" in msg for msg in logger.infos)\n\ndef test_ensure_compatible_candle_time_adapted_start_time_too_short():\n    args = base_args()\n    tf_sec = common_enums.TimeFramesMinutes[args[\"time_frame\"]] * common_constants.MINUTE_TO_SECONDS\n    first_open_time = 1000000\n    last_open_time = 1000000 + 30 * tf_sec\n    first_candle_time = first_open_time + 25 * tf_sec\n    last_candle_time = last_open_time\n    args[\"first_traded_symbols_time\"] = first_open_time + 30 * tf_sec\n    # This will adapt, but duration will be too short\n    with pytest.raises(errors.InvalidBacktestingDataError) as exc:\n        src_backtesting_data_collector.ensure_compatible_candle_time(\n            **args,\n            first_open_time=first_open_time,\n            last_open_time=last_open_time,\n            first_candle_time=first_candle_time,\n            last_candle_time=last_candle_time,\n        )\n    assert \"adapted backtesting start time starts too late\" in str(exc.value)\n\n\n\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/backtesting/test_collect_data_and_run_backtesting.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.profiles.profile_data as commons_profile_data\nimport octobot_commons.constants as commons_constants\n\nimport octobot_trading.api\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_clients_cache as ccxt_clients_cache\nimport octobot_trading.util.test_tools.exchange_data as exchange_data_import\n\nimport tentacles.Meta.Keywords.scripting_library as scripting_library\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\nimport tentacles.Trading.Mode.index_trading_mode.index_trading as index_trading\n\n\n@pytest.fixture\ndef trading_mode_tentacles_data() -> commons_profile_data.TentaclesData:\n    distribution = [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 50.0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_distribution.DISTRIBUTION_VALUE: 30.0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"USD\",  # Will be replaced by reference market\n            index_distribution.DISTRIBUTION_VALUE: 20.0,\n        },\n    ]\n    \n    # Create test trading mode config\n    trading_mode_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: distribution,\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 5.0,\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"test_profile\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"test_profile\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.0,\n            }\n        ],\n    }\n    return commons_profile_data.TentaclesData(\n        name=index_trading.IndexTradingMode.get_name(),\n        config=trading_mode_config\n    )\n\n@pytest.mark.asyncio\nasync def test_collect_candles_without_backend_and_run_backtesting(trading_mode_tentacles_data):\n    # 1. init strategy\n    exchange_data = exchange_data_import.ExchangeData()\n    # run backtesting for 200 days\n    days = 200\n    profile_data = scripting_library.create_index_config_from_tentacles_config(\n        tentacles_config=[trading_mode_tentacles_data],\n        exchange=\"binanceus\",\n        starting_funds=1000,\n        backtesting_start_time_delta=days * commons_constants.DAYS_TO_SECONDS\n    )\n\n    # 2. collect candles\n    ccxt_clients_cache._MARKETS_BY_EXCHANGE.clear()\n    await scripting_library.init_exchange_market_status_and_populate_backtesting_exchange_data(\n        exchange_data, profile_data\n    )\n    # cached markets have been updated and now contain this exchange markets\n    assert len(ccxt_clients_cache._MARKETS_BY_EXCHANGE) == 1\n    # ensure collected datas are correct\n    assert len(exchange_data.markets) == 2\n    assert sorted([market.symbol for market in exchange_data.markets]) == [\"BTC/USDT\", \"ETH/USDT\"]\n    for market in exchange_data.markets:\n        assert market.time_frame == commons_enums.TimeFrames.ONE_DAY.value\n        assert days - 1 <= len(market.close) <= days\n        assert days - 1 <= len(market.open) <= days\n        assert days - 1 <= len(market.high) <= days\n        assert days - 1 <= len(market.low) <= days\n        assert days - 1 <= len(market.volume) <= days\n        assert days - 1 <= len(market.time) <= days\n\n    starting_portfolio = profile_data.backtesting_context.starting_portfolio\n    assert starting_portfolio == {\n        \"USDT\": 1000,\n    }\n    # 3. run backtesting\n    async with scripting_library.init_and_run_backtesting(\n        exchange_data, profile_data\n    ) as independent_backtesting:\n        # backtesting completed, make sure it executed correctly\n        for exchange_id in independent_backtesting.octobot_backtesting.exchange_manager_ids:\n            exchange_manager = octobot_trading.api.get_exchange_manager_from_exchange_id(exchange_id)\n            ending_portfolio = octobot_trading.api.get_portfolio(exchange_manager, as_decimal=False)\n            assert ending_portfolio != starting_portfolio\n            assert \"ETH\" in ending_portfolio\n            assert \"BTC\" in ending_portfolio\n            assert \"USDT\" in ending_portfolio\n            trades = octobot_trading.api.get_trade_history(exchange_manager)\n            # at least 2 trades are expected, one for each symbol\n            assert len(trades) >= 2\n            # backtesting is not stopped yet\n        assert independent_backtesting.stopped is False\n\n    # 4. ensure backtesting is stopped\n    assert independent_backtesting.stopped is True\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/backtesting/test_run_data.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.backtesting.run_data_analysis as run_data_analysis\nimport octobot_trading.enums as trading_enums\nimport octobot_commons.enums as commons_enums\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop\nfrom tentacles.Meta.Keywords.scripting_library.tests.backtesting.data_store import default_price_data, \\\n    default_trades_data, default_portfolio_data, default_portfolio_historical_value, default_pnl_historical_value, \\\n    default_funding_fees_data, default_realized_pnl_history, default_spot_metadata\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_plot_historical_portfolio_value(default_price_data, default_trades_data, default_portfolio_data,\n                                               default_portfolio_historical_value, default_funding_fees_data,\n                                               default_spot_metadata):\n    expected_time_data = [candle[commons_enums.PriceIndexes.IND_PRICE_TIME.value]\n                          for candle in default_price_data[\"BTC/USDT\"]]\n    await _test_historical_portfolio_values(default_price_data, default_trades_data, default_portfolio_data,\n                                            default_funding_fees_data, expected_time_data,\n                                            default_portfolio_historical_value,\n                                            \"spot\",\n                                            default_spot_metadata)\n\n\nasync def test_get_historical_pnl(default_price_data, default_trades_data, default_pnl_historical_value,\n                                  default_realized_pnl_history, default_spot_metadata):\n    # expected_time_data start at the 1st time data with a default_pnl_historical_value at 0\n    expected_time_data = \\\n        [default_price_data[\"BTC/USDT\"][0][commons_enums.PriceIndexes.IND_PRICE_TIME.value]] + \\\n        [trade[commons_enums.PlotAttributes.X.value]\n         for trade in default_trades_data[\"BTC/USDT\"]\n         if trade[commons_enums.PlotAttributes.SIDE.value] == trading_enums.TradeOrderSide.SELL.value] + \\\n        [default_price_data[\"BTC/USDT\"][-1][commons_enums.PriceIndexes.IND_PRICE_TIME.value]]\n    cumulative_pnl_historical_value = [default_pnl_historical_value[0]]\n    for value in default_pnl_historical_value[1:]:\n        cumulative_pnl_historical_value.append(cumulative_pnl_historical_value[-1] + value)\n    await _test_historical_pnl_values_from_trades(default_price_data, default_trades_data, [], False, True, False,\n                                                  expected_time_data, default_pnl_historical_value,\n                                                  cumulative_pnl_historical_value,\n                                                  \"spot\", default_spot_metadata)\n\n    expected_time_data = [i for i in range(len(cumulative_pnl_historical_value))]\n    await _test_historical_pnl_values_from_trades(default_price_data, default_trades_data, default_realized_pnl_history,\n                                                  True, True, True, expected_time_data, default_pnl_historical_value,\n                                                  cumulative_pnl_historical_value,\n                                                  \"spot\", default_spot_metadata)\n    await _test_historical_pnl_values_from_trades(default_price_data, default_trades_data, default_realized_pnl_history,\n                                                  False, False, True, expected_time_data, default_pnl_historical_value,\n                                                  cumulative_pnl_historical_value,\n                                                  \"spot\", default_spot_metadata)\n\n\nasync def test_total_paid_fees(default_trades_data):\n    usdt_fees = sum(trade[commons_enums.DBRows.FEES_AMOUNT.value]\n                    for trade in default_trades_data[\"BTC/USDT\"]\n                    if trade[commons_enums.DBRows.FEES_CURRENCY.value] == \"USDT\")\n    btc_fees_in_usdt = sum(trade[commons_enums.DBRows.FEES_AMOUNT.value] * trade[commons_enums.PlotAttributes.Y.value]\n                           for trade in default_trades_data[\"BTC/USDT\"]\n                           if trade[commons_enums.DBRows.FEES_CURRENCY.value] == \"BTC\")\n    with mock.patch.object(run_data_analysis, \"get_transactions\",\n                           mock.AsyncMock(return_value=[])) as get_transactions_mock:\n        assert round(await run_data_analysis.total_paid_fees(None, default_trades_data[\"BTC/USDT\"]), 15) == \\\n               round(usdt_fees + btc_fees_in_usdt, 15)\n        get_transactions_mock.assert_called_once()\n\n\nasync def _test_historical_portfolio_values(price_data, trades_data, portfolio_data, funding_fees_data,\n                                            expected_time_data, expected_value_data, exchange_type,\n                                            spot_metadata):\n    plotted_element = mock.Mock()\n    with mock.patch.object(run_data_analysis, \"load_historical_values\",\n                           mock.AsyncMock(return_value=(price_data, trades_data, portfolio_data, exchange_type,\n                                                        spot_metadata, spot_metadata))) \\\n            as load_historical_values_mock, \\\n         mock.patch.object(run_data_analysis, \"get_transactions\",\n                           mock.AsyncMock(return_value=funding_fees_data)) \\\n            as get_transactions_mock:\n        await run_data_analysis.plot_historical_portfolio_value(\"meta_database\", plotted_element,\n                                                                exchange=\"exchange\", own_yaxis=True)\n        load_historical_values_mock.assert_called_once_with(\"meta_database\", \"exchange\")\n        get_transactions_mock.assert_called_once_with(\"meta_database\",\n                                                      transaction_type=trading_enums.TransactionType.FUNDING_FEE.value)\n        plotted_element.plot.assert_called_once_with(\n            mode=\"scatter\",\n            x=expected_time_data,\n            y=expected_value_data,\n            title=\"Portfolio value\",\n            own_yaxis=True\n        )\n\n\nasync def _test_historical_pnl_values_from_trades(price_data, trades_data, pnl_data, include_cumulative,\n                                                  include_unitary,\n                                                  x_as_trade_count, expected_time_data, expected_value_data,\n                                                  expected_cumulative_values,\n                                                  exchange_type, spot_metadata):\n    plotted_element = mock.Mock()\n    with mock.patch.object(run_data_analysis, \"load_historical_values\",\n                           mock.AsyncMock(return_value=(price_data, trades_data, None, exchange_type, spot_metadata,\n                                                        spot_metadata))) \\\n            as load_historical_values_mock, \\\n         mock.patch.object(run_data_analysis, \"get_transactions\",\n                           mock.AsyncMock(return_value=pnl_data)) \\\n            as get_transactions_mock:\n        await run_data_analysis._get_historical_pnl(\"meta_database\", plotted_element, include_cumulative,\n                                                    include_unitary,\n                                                    exchange=\"exchange\", x_as_trade_count=x_as_trade_count,\n                                                    own_yaxis=True)\n        load_historical_values_mock.assert_called_once_with(\"meta_database\", \"exchange\")\n        get_transactions_mock.assert_called_once_with(\"meta_database\",\n                                                      transaction_types=(\n                                                          trading_enums.TransactionType.TRADING_FEE.value,\n                                                          trading_enums.TransactionType.FUNDING_FEE.value,\n                                                          trading_enums.TransactionType.REALISED_PNL.value,\n                                                          trading_enums.TransactionType.CLOSE_REALISED_PNL.value)\n                                                      )\n        if include_cumulative:\n            assert plotted_element.plot.call_count == 2\n        else:\n            if include_unitary:\n                plotted_element.plot.assert_called_once_with(\n                    kind=\"bar\",\n                    x=expected_time_data,\n                    y=expected_value_data,\n                    x_type=\"tick0\" if x_as_trade_count else \"date\",\n                    title=\"P&L per trade\",\n                    own_yaxis=True\n                )\n            else:\n                plotted_element.assert_not_called()\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/configuration/__init__.py",
    "content": "import os\nimport pathlib\n\nimport pytest\nimport pytest_asyncio\n\nimport octobot_commons.constants as commons_constants\nimport octobot_backtesting.backtesting as backtesting\nimport octobot_backtesting.constants as backtesting_constants\nimport octobot_backtesting.time as backtesting_time\nimport octobot_trading.exchanges as exchanges\n\nfrom octobot_commons.tests.test_config import load_test_config\n\npytestmark = pytest.mark.asyncio\n\n\nDEFAULT_EXCHANGE_NAME = \"binance\"\nTEST_CONFIG_FOLDER = pathlib.Path(os.path.abspath(__file__)).parent.parent\n\n\n@pytest_asyncio.fixture\nasync def backtesting_config(request):\n    config = load_test_config(test_folder=TEST_CONFIG_FOLDER)\n    config[backtesting_constants.CONFIG_BACKTESTING] = {}\n    config[backtesting_constants.CONFIG_BACKTESTING][commons_constants.CONFIG_ENABLED_OPTION] = True\n    if hasattr(request, \"param\"):\n        ref_market = request.param\n        config[commons_constants.CONFIG_TRADING][commons_constants.CONFIG_TRADER_REFERENCE_MARKET] = ref_market\n    return config\n\n\n@pytest_asyncio.fixture\nasync def fake_backtesting(backtesting_config):\n    return backtesting.Backtesting(\n        config=backtesting_config,\n        exchange_ids=[],\n        matrix_id=\"\",\n        backtesting_files=[],\n    )\n\n\n@pytest_asyncio.fixture\nasync def backtesting_exchange_manager(request, backtesting_config, fake_backtesting):\n    config = None\n    exchange_name = DEFAULT_EXCHANGE_NAME\n    is_spot = True\n    is_margin = False\n    is_future = False\n    if hasattr(request, \"param\"):\n        config, exchange_name, is_spot, is_margin, is_future = request.param\n\n    if config is None:\n        config = backtesting_config\n    exchange_manager_instance = exchanges.ExchangeManager(config, exchange_name)\n    exchange_manager_instance.is_backtesting = True\n    exchange_manager_instance.is_spot_only = is_spot\n    exchange_manager_instance.is_margin = is_margin\n    exchange_manager_instance.is_future = is_future\n    exchange_manager_instance.use_cached_markets = False\n    exchange_manager_instance.backtesting = fake_backtesting\n    exchange_manager_instance.backtesting.time_manager = backtesting_time.TimeManager(config)\n    await exchange_manager_instance.initialize(exchange_config_by_exchange=None)\n    yield exchange_manager_instance\n    await exchange_manager_instance.stop()\n\n\n@pytest_asyncio.fixture\nasync def backtesting_trader(backtesting_config, backtesting_exchange_manager):\n    trader_instance = exchanges.TraderSimulator(backtesting_config, backtesting_exchange_manager)\n    await trader_instance.initialize()\n    return backtesting_config, backtesting_exchange_manager, trader_instance\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/configuration/test_indexes_configuration.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.profiles.profile_data as commons_profile_data\nimport octobot_commons.constants as commons_constants\n\nimport tentacles.Trading.Mode.index_trading_mode.index_trading as index_trading\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\nimport tentacles.Meta.Keywords.scripting_library.configuration.indexes_configuration as indexes_configuration\n\n\ndef test_create_index_config_from_tentacles_config():\n    # Create test distribution\n    distribution = [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 50.0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_distribution.DISTRIBUTION_VALUE: 30.0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"USD\",  # Should be replaced by reference market\n            index_distribution.DISTRIBUTION_VALUE: 20.0,\n        },\n    ]\n    \n    # Create test trading mode config\n    trading_mode_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: distribution,\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 5.0,\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"test_profile\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"test_profile\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.0,\n            }\n        ],\n    }\n    \n    # Create tentacles config\n    tentacles_config = [\n        commons_profile_data.TentaclesData(\n            name=index_trading.IndexTradingMode.get_name(),\n            config=trading_mode_config\n        )\n    ]\n    \n    # Test parameters\n    exchange = \"binance\"\n    starting_funds = 10000.0\n    backtesting_start_time_delta = 86400.0  # 1 day in seconds\n    \n    # Call the function\n    result = indexes_configuration.create_index_config_from_tentacles_config(\n        tentacles_config, exchange, starting_funds, backtesting_start_time_delta\n    )\n    \n    # Assertions\n    assert isinstance(result, commons_profile_data.ProfileData)\n    assert result.profile_details.name == \"serverless\"\n    assert result.trading.reference_market == \"USDC\"  # binance default\n    assert result.trading.risk == 0.5\n    assert len(result.exchanges) == 1\n    assert result.exchanges[0].internal_name == exchange\n    assert result.exchanges[0].exchange_type == commons_constants.CONFIG_EXCHANGE_SPOT\n    \n    # Check currencies (BTC and ETH, not USD which should be replaced by reference market)\n    assert len(result.crypto_currencies) == 2\n    trading_pairs = {curr.name: curr.trading_pairs for curr in result.crypto_currencies}\n    assert [\"BTC/USDC\"] == trading_pairs[\"BTC\"]\n    assert [\"ETH/USDC\"] == trading_pairs[\"ETH\"]\n    \n    # Check trader settings\n    assert result.trader.enabled is True\n    \n    # Check tentacles config\n    assert len(result.tentacles) == 1\n    assert result.tentacles[0].name == index_trading.IndexTradingMode.get_name()\n    assert index_trading.IndexTradingModeProducer.INDEX_CONTENT in result.tentacles[0].config\n    \n    # Check that USD was replaced by reference market in distribution\n    distribution_names = [\n        item[index_distribution.DISTRIBUTION_NAME] \n        for item in result.tentacles[0].config[index_trading.IndexTradingModeProducer.INDEX_CONTENT]\n    ]\n    assert \"USD\" not in distribution_names\n    assert \"USDC\" in distribution_names  # binance's reference market\n    \n    # Check backtesting config\n    assert result.backtesting_context is not None\n    assert [exchange] == result.backtesting_context.exchanges\n    assert result.backtesting_context.start_time_delta == backtesting_start_time_delta\n    assert {\"USDC\": starting_funds} == result.backtesting_context.starting_portfolio\n\n\ndef test_generate_index_config():\n    # Create test distribution\n    distribution = [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 50.0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_distribution.DISTRIBUTION_VALUE: 30.0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"USDT\",\n            index_distribution.DISTRIBUTION_VALUE: 20.0,\n        },\n    ]\n    \n    # Test parameters\n    rebalance_cap = 5.0\n    selected_rebalance_trigger_profile = \"profile1\"\n    rebalance_trigger_profiles = [\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile1\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.0,\n        },\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile2\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 10.0,\n        },\n    ]\n    reference_market = \"USDT\"\n    exchange = \"binance\"\n    min_funds = 1000.0\n    coins_by_symbol = {\n        \"BTC\": \"BTC\",\n        \"ETH\": \"ETH\",\n        \"USDT\": \"USDT\",\n    }\n    disabled_backtesting = False\n    backtesting_start_time_delta = 172800.0  # 2 days in seconds\n    \n    # Call the function\n    result = indexes_configuration.generate_index_config(\n        distribution, rebalance_cap, selected_rebalance_trigger_profile, \n        rebalance_trigger_profiles, reference_market, exchange, min_funds, \n        coins_by_symbol, disabled_backtesting, backtesting_start_time_delta\n    )\n    \n    # Assertions - check that result is a dict\n    assert isinstance(result, dict)\n    \n    # Check profile details\n    assert \"profile_details\" in result\n    assert result[\"profile_details\"][\"name\"] == \"serverless\"\n    \n    # Check trading config\n    assert \"trading\" in result\n    assert result[\"trading\"][\"reference_market\"] == reference_market\n    assert result[\"trading\"][\"risk\"] == 0.5\n    \n    # Check exchanges\n    assert \"exchanges\" in result\n    assert len(result[\"exchanges\"]) == 1\n    assert result[\"exchanges\"][0][\"internal_name\"] == exchange\n    \n    # Check crypto currencies (should not include reference market)\n    assert \"crypto_currencies\" in result\n    assert len(result[\"crypto_currencies\"]) == 2  # BTC and ETH, not USDT (reference market)\n    trading_pairs = {curr[\"name\"]: curr[\"trading_pairs\"] for curr in result[\"crypto_currencies\"]}\n    assert [\"BTC/USDT\"] == trading_pairs[\"BTC\"]\n    assert [\"ETH/USDT\"] == trading_pairs[\"ETH\"]\n    \n    # Check trader\n    assert \"trader\" in result\n    assert result[\"trader\"][\"enabled\"] is True\n    \n    # Check tentacles\n    assert \"tentacles\" in result\n    assert len(result[\"tentacles\"]) == 1\n    tentacle_config = result[\"tentacles\"][0]\n    assert tentacle_config[\"name\"] == index_trading.IndexTradingMode.get_name()\n    assert \"config\" in tentacle_config\n    \n    # Check index trading config\n    config = tentacle_config[\"config\"]\n    assert config[index_trading.IndexTradingModeProducer.INDEX_CONTENT] == distribution\n    assert config[index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT] == rebalance_cap\n    assert config[index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE] == selected_rebalance_trigger_profile\n    assert config[index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES] == rebalance_trigger_profiles\n    assert config[index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY] == index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE.value\n    assert config[index_trading.IndexTradingModeProducer.SELL_UNINDEXED_TRADED_COINS] is True\n    assert config[index_trading.IndexTradingModeProducer.REFRESH_INTERVAL] == 1\n    \n    # Check backtesting config\n    assert \"backtesting_context\" in result\n    backtesting = result[\"backtesting_context\"]\n    assert backtesting[\"exchanges\"] == [exchange]\n    assert backtesting[\"start_time_delta\"] == backtesting_start_time_delta\n    assert {\"USDT\": min_funds * 10} == backtesting[\"starting_portfolio\"]\n    \n    # Test with disabled backtesting\n    result_no_backtesting = indexes_configuration.generate_index_config(\n        distribution, rebalance_cap, selected_rebalance_trigger_profile,\n        rebalance_trigger_profiles, reference_market, exchange, min_funds,\n        coins_by_symbol, True, backtesting_start_time_delta\n    )\n    assert \"exchanges\" not in result_no_backtesting[\"backtesting_context\"]\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/configuration/test_profile_data_configuration.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Mode.index_trading_mode.index_trading as index_trading\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.profiles.profile_data as commons_profile_data\nimport tentacles.Meta.Keywords.scripting_library as scripting_library\n\n\n\ndef test_register_historical_configs_adds_traded_pairs():\n    # Master has no traded pairs, historical has one\n    master = scripting_library.minimal_profile_data()\n    tentacle_name = \"TestTentacle\"\n    master.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config={})]\n    # Historical profile with a traded pair\n    historical = scripting_library.minimal_profile_data()\n    historical.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config={})]\n    scripting_library.add_traded_symbols(historical, [\"BTC/USDT\"])\n    historicals = {1000.0: historical}\n    assert [] == scripting_library.get_traded_symbols(master)\n    scripting_library.register_historical_configs(master, historicals, True, False)\n    # Master should now have the traded pair\n    assert [\"BTC/USDT\"] == scripting_library.get_traded_symbols(master)\n\n\ndef test_register_historical_configs_registers_historical_tentacle_config():\n    # Master and historical have different tentacle config dicts\n    master = scripting_library.minimal_profile_data()\n    tentacle_name = \"TestTentacle\"\n    master_config = {\"foo\": 1}\n    master.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config=master_config)]\n    historical_1 = scripting_library.minimal_profile_data()\n    hist_config_1 = {\"foo\": 2}\n    historical_1.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config=hist_config_1)]\n    historical_2 = scripting_library.minimal_profile_data()\n    hist_config_2 = {\"foo\": 3}\n    historical_2.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config=hist_config_2)]\n    historicals = {1000.0: historical_1, 2000.0: historical_2}\n    scripting_library.register_historical_configs(master, historicals, False, False)\n    # Master config should now have a historical config registered\n    assert commons_constants.CONFIG_HISTORICAL_CONFIGURATION in master_config\n    assert len(master_config[commons_constants.CONFIG_HISTORICAL_CONFIGURATION]) == 2\n    assert master_config[commons_constants.CONFIG_HISTORICAL_CONFIGURATION][0][0] == 2000.0\n    assert master_config[commons_constants.CONFIG_HISTORICAL_CONFIGURATION][0][1] == hist_config_2\n    assert master_config[commons_constants.CONFIG_HISTORICAL_CONFIGURATION][1][0] == 1000.0\n    assert master_config[commons_constants.CONFIG_HISTORICAL_CONFIGURATION][1][1] == hist_config_1\n\n\ndef test_register_historical_configs_applies_master_edits():\n    # Master has a config with a special field, historical does not\n    master = scripting_library.minimal_profile_data()\n    tentacle_name = \"TestTentacle\"\n    special_key = \"special\"\n    master_config = {\n        special_key: 42, \n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"plop1\", \n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"plop1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 4\n            },\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"plop2\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20\n            }\n        ], \n        index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY: index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE.value\n    }\n    master.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config=master_config)]\n    historical_1 = scripting_library.minimal_profile_data()\n    hist_config_1 = {}\n    historical_1.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config=hist_config_1)]\n    historical_2 = scripting_library.minimal_profile_data()\n    hist_config_2 = {special_key: 1}\n    historical_2.tentacles = [commons_profile_data.TentaclesData(name=tentacle_name, config=hist_config_2)]\n    historicals = {1000.0: historical_1, 2000.0: historical_2}\n\n    scripting_library.register_historical_configs(master, historicals, False, True)\n    # no update as tentacle_name is not configurable tentacles and config keys\n    assert hist_config_1 == {}\n    assert hist_config_2 == {special_key: 1}\n\n    # now using IndexTradingMode: a whitelisted tentacle\n    for profile_data in (master, historical_1, historical_2):\n        profile_data.tentacles[0].name = index_trading.IndexTradingMode.get_name()\n\n    scripting_library.register_historical_configs(master, historicals, False, True)\n    # configurable tentacles abd config keys are applied to historical configs\n    assert hist_config_1 == {\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"plop1\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"plop1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 4\n            },\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"plop2\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20\n            }\n        ], \n        index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY: index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE.value\n    }\n    assert hist_config_2 == {\n        special_key: 1, \n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"plop1\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"plop1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 4\n            },\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"plop2\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20\n            }\n        ], \n        index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY: index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE.value\n    }\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/exchanges/__init__.py",
    "content": "import os\nimport pathlib\n\nimport pytest\nimport pytest_asyncio\n\nimport octobot_commons.constants as commons_constants\nimport octobot_backtesting.backtesting as backtesting\nimport octobot_backtesting.constants as backtesting_constants\nimport octobot_backtesting.time as backtesting_time\nimport octobot_trading.exchanges as exchanges\n\nfrom octobot_commons.tests.test_config import load_test_config\n\npytestmark = pytest.mark.asyncio\n\n\nDEFAULT_EXCHANGE_NAME = \"binance\"\nTEST_CONFIG_FOLDER = pathlib.Path(os.path.abspath(__file__)).parent.parent\n\n\n@pytest_asyncio.fixture\nasync def backtesting_config(request):\n    config = load_test_config(test_folder=TEST_CONFIG_FOLDER)\n    config[backtesting_constants.CONFIG_BACKTESTING] = {}\n    config[backtesting_constants.CONFIG_BACKTESTING][commons_constants.CONFIG_ENABLED_OPTION] = True\n    if hasattr(request, \"param\"):\n        ref_market = request.param\n        config[commons_constants.CONFIG_TRADING][commons_constants.CONFIG_TRADER_REFERENCE_MARKET] = ref_market\n    return config\n\n\n@pytest_asyncio.fixture\nasync def fake_backtesting(backtesting_config):\n    return backtesting.Backtesting(\n        config=backtesting_config,\n        exchange_ids=[],\n        matrix_id=\"\",\n        backtesting_files=[],\n    )\n\n\n@pytest_asyncio.fixture\nasync def backtesting_exchange_manager(request, backtesting_config, fake_backtesting):\n    config = None\n    exchange_name = DEFAULT_EXCHANGE_NAME\n    is_spot = True\n    is_margin = False\n    is_future = False\n    if hasattr(request, \"param\"):\n        config, exchange_name, is_spot, is_margin, is_future = request.param\n\n    if config is None:\n        config = backtesting_config\n    exchange_manager_instance = exchanges.ExchangeManager(config, exchange_name)\n    exchange_manager_instance.is_backtesting = True\n    exchange_manager_instance.is_spot_only = is_spot\n    exchange_manager_instance.is_margin = is_margin\n    exchange_manager_instance.is_future = is_future\n    exchange_manager_instance.use_cached_markets = False\n    exchange_manager_instance.backtesting = fake_backtesting\n    exchange_manager_instance.backtesting.time_manager = backtesting_time.TimeManager(config)\n    await exchange_manager_instance.initialize(exchange_config_by_exchange=None)\n    yield exchange_manager_instance\n    await exchange_manager_instance.stop()\n\n\n@pytest_asyncio.fixture\nasync def backtesting_trader(backtesting_config, backtesting_exchange_manager):\n    trader_instance = exchanges.TraderSimulator(backtesting_config, backtesting_exchange_manager)\n    await trader_instance.initialize()\n    return backtesting_config, backtesting_exchange_manager, trader_instance\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/__init__.py",
    "content": "# Copyright\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/__init__.py",
    "content": "# Copyright\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_create_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\nimport decimal\nimport os\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.position_size as position_size\nimport tentacles.Meta.Keywords.scripting_library.orders.grouping as grouping\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as errors\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.modes.script_keywords as script_keywords\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context, mock_context, symbol_market, \\\n    skip_if_octobot_trading_mocking_disabled\nfrom tentacles.Meta.Keywords.scripting_library.tests.exchanges import backtesting_trader, backtesting_config, \\\n    backtesting_exchange_manager, fake_backtesting\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_create_order_instance(mock_context):\n    with mock.patch.object(create_order, \"_get_order_quantity_and_side\",\n                           mock.AsyncMock(return_value=(decimal.Decimal(1), \"sell\"))) \\\n            as _get_order_quantity_and_side_mock, \\\n            mock.patch.object(create_order, \"_get_order_details\",\n                              mock.AsyncMock(return_value=(1, 2, 3, 4, 5, 6, 7, 8, 9))) \\\n            as _get_order_details_mock, \\\n            mock.patch.object(script_keywords, \"get_price_with_offset\", mock.AsyncMock(return_value=42)) as get_offset_mock, \\\n            mock.patch.object(create_order, \"_create_order\", mock.AsyncMock()) as _create_order_mock:\n        with mock.patch.object(create_order, \"_paired_order_is_closed\", mock.Mock(return_value=True)) \\\n             as _paired_order_is_closed_mock:\n            order = mock.Mock(is_open=mock.Mock(return_value=False))\n            assert [] == await create_order.create_order_instance(\n                mock_context, \"side\", \"symbol\", \"order_amount\", \"order_target_position\",\n                \"stop_loss_offset\", \"stop_loss_tag\", \"stop_loss_type\", \"stop_loss_group\",\n                \"take_profit_offset\", \"take_profit_tag\", \"take_profit_type\", \"take_profit_group\",\n                \"order_type_name\", \"order_offset\", \"order_min_offset\", \"order_max_offset\", \"order_limit_offset\",\n                \"slippage_limit\", \"time_limit\", \"reduce_only\", \"post_only\", \"tag\", \"group\", [order])\n            _paired_order_is_closed_mock.assert_called_once_with(mock_context, \"group\")\n            _get_order_quantity_and_side_mock.assert_not_called()\n            _get_order_details_mock.assert_not_called()\n            get_offset_mock.assert_not_called()\n            _create_order_mock.assert_not_called()\n        with mock.patch.object(create_order, \"_paired_order_is_closed\", mock.Mock(return_value=False)) \\\n             as _paired_order_is_closed_mock:\n            order = mock.Mock(is_open=mock.Mock(return_value=False))\n            await create_order.create_order_instance(\n                mock_context, \"side\", \"symbol\", \"order_amount\", \"order_target_position\",\n                \"stop_loss_offset\", \"stop_loss_tag\", \"stop_loss_type\", \"stop_loss_group\",\n                \"take_profit_offset\", \"take_profit_tag\", \"take_profit_type\", \"take_profit_group\",\n                \"order_type_name\", \"order_offset\", \"order_min_offset\", \"order_max_offset\", \"order_limit_offset\",\n                \"slippage_limit\", \"time_limit\", \"reduce_only\", \"post_only\", \"tag\", \"group\", [order])\n            _paired_order_is_closed_mock.assert_called_once_with(mock_context, \"group\")\n            _get_order_quantity_and_side_mock.assert_called_once_with(mock_context, \"order_amount\",\n                                                                      \"order_target_position\", \"order_type_name\",\n                                                                      \"side\", \"reduce_only\", False)\n            _get_order_details_mock.assert_called_once_with(mock_context, \"order_type_name\", \"sell\", \"order_offset\",\n                                                            \"reduce_only\", \"order_limit_offset\")\n            assert get_offset_mock.call_count == 2\n            _create_order_mock.assert_called_once_with(\n                context=mock_context, symbol=\"symbol\", order_quantity=decimal.Decimal(1), order_price=2, tag=\"tag\",\n                order_type_name=\"order_type_name\", input_side=\"side\",\n                side=\"sell\", final_side=3, order_type=1, order_min_offset=\"order_min_offset\", max_offset_val=7,\n                reduce_only=4, group=\"group\",\n                stop_loss_price=42, stop_loss_tag=\"stop_loss_tag\", stop_loss_type=\"stop_loss_type\",\n                stop_loss_group=\"stop_loss_group\",\n                take_profit_price=42, take_profit_tag=\"take_profit_tag\", take_profit_type=\"take_profit_type\",\n                take_profit_group=\"take_profit_group\",\n                wait_for=[order],\n                truncate=False,\n                order_amount='order_amount', order_target_position='order_target_position')\n\n\nasync def test_paired_order_is_closed(mock_context, skip_if_octobot_trading_mocking_disabled):\n    # skip_if_octobot_trading_mocking_disabled oco_group, \"get_group_open_orders\"\n    assert create_order._paired_order_is_closed(mock_context, None) is False\n    oco_group = grouping.create_one_cancels_the_other_group(mock_context)\n    assert create_order._paired_order_is_closed(mock_context, oco_group) is False\n    order = mock.Mock()\n    order_2 = mock.Mock()\n    order_2.is_closed = mock.Mock(return_value=True)\n    if os.getenv('CYTHON_IGNORE'):\n        return\n    with mock.patch.object(oco_group, \"get_group_open_orders\", mock.Mock(return_value=[order, order_2])) as \\\n        get_group_open_orders_mock:\n        with mock.patch.object(order, \"is_closed\", mock.Mock(return_value=True)) as is_closed_mock:\n            assert create_order._paired_order_is_closed(mock_context, oco_group) is True\n            is_closed_mock.assert_called_once()\n            get_group_open_orders_mock.assert_called_once()\n            get_group_open_orders_mock.reset_mock()\n        with mock.patch.object(order, \"is_closed\", mock.Mock(return_value=False)) as is_closed_mock:\n            assert create_order._paired_order_is_closed(mock_context, oco_group) is False\n            is_closed_mock.assert_called_once()\n            get_group_open_orders_mock.assert_called_once()\n    order.order_group = None\n    null_context.just_created_orders = [order]\n    with mock.patch.object(order, \"is_closed\", mock.Mock(return_value=True)) as is_closed_mock:\n        assert create_order._paired_order_is_closed(null_context, oco_group) is False\n        is_closed_mock.assert_not_called()\n        order.order_group = oco_group\n        assert create_order._paired_order_is_closed(null_context, oco_group) is True\n        is_closed_mock.assert_called_once()\n        order.order_group = mock.Mock()\n        is_closed_mock.reset_mock()\n        assert create_order._paired_order_is_closed(null_context, oco_group) is False\n        is_closed_mock.assert_not_called()\n\n\nasync def test_use_total_holding():\n    with mock.patch.object(create_order, \"_is_stop_order\", mock.Mock(return_value=False)) as _is_stop_order_mock:\n        assert create_order._use_total_holding(\"type\") is False\n        _is_stop_order_mock.assert_called_once_with(\"type\")\n    with mock.patch.object(create_order, \"_is_stop_order\", mock.Mock(return_value=True)) as _is_stop_order_mock:\n        assert create_order._use_total_holding(\"type2\") is True\n        _is_stop_order_mock.assert_called_once_with(\"type2\")\n\n\nasync def test_is_stop_order():\n    assert create_order._is_stop_order(\"\") is False\n    assert create_order._is_stop_order(\"market\") is False\n    assert create_order._is_stop_order(\"limit\") is False\n    assert create_order._is_stop_order(\"stop_loss\") is True\n    assert create_order._is_stop_order(\"stop_market\") is True\n    assert create_order._is_stop_order(\"stop_limit\") is True\n    assert create_order._is_stop_order(\"trailing_stop_loss\") is True\n    assert create_order._is_stop_order(\"trailing_market\") is False\n    assert create_order._is_stop_order(\"trailing_limit\") is False\n\n\nasync def test_get_order_quantity_and_side(null_context):\n    # order_amount and order_target_position are both not set\n    with pytest.raises(errors.InvalidArgumentError):\n        await create_order._get_order_quantity_and_side(null_context, None, None, \"\", \"\", True, False)\n\n    # order_amount and order_target_position are set\n    with pytest.raises(errors.InvalidArgumentError):\n        await create_order._get_order_quantity_and_side(null_context, 1, 2, \"\", \"\", True, False)\n\n    # order_amount but no side\n    with pytest.raises(errors.InvalidArgumentError):\n        await create_order._get_order_quantity_and_side(null_context, 1, None, \"\", None, True, False)\n    with pytest.raises(errors.InvalidArgumentError):\n        await create_order._get_order_quantity_and_side(null_context, 1, None, \"\", \"fsdsfds\", True, True), False\n\n    with mock.patch.object(position_size, \"get_amount\",\n                           mock.AsyncMock(return_value=decimal.Decimal(1))) as get_amount_mock:\n        with mock.patch.object(create_order, \"_use_total_holding\",\n                               mock.Mock(return_value=False)) as _use_total_holding_mock, \\\n                mock.patch.object(create_order, \"_is_stop_order\",\n                                  mock.Mock(return_value=False)) as _is_stop_order_mock:\n            assert await create_order._get_order_quantity_and_side(null_context, 1, None, \"\", \"sell\", True, False) \\\n                   == (decimal.Decimal(1), \"sell\")\n            get_amount_mock.assert_called_once_with(null_context, 1, \"sell\", True, False, use_total_holding=False,\n                                                    unknown_portfolio_on_creation=False)\n            get_amount_mock.reset_mock()\n            _is_stop_order_mock.assert_called_once_with(\"\")\n            _use_total_holding_mock.assert_called_once_with(\"\")\n        with mock.patch.object(create_order, \"_use_total_holding\",\n                               mock.Mock(return_value=True)) as _use_total_holding_mock, \\\n                mock.patch.object(create_order, \"_is_stop_order\",\n                                 mock.Mock(return_value=True)) as _is_stop_order_mock:\n            assert await create_order._get_order_quantity_and_side(null_context, 1, None, \"order_type\", \"sell\", False,\n                                                                   True) \\\n                   == (decimal.Decimal(1), \"sell\")\n            get_amount_mock.assert_called_once_with(null_context, 1, \"sell\", False, True, use_total_holding=True,\n                                                    unknown_portfolio_on_creation=True)\n            get_amount_mock.reset_mock()\n            _is_stop_order_mock.assert_called_once_with(\"order_type\")\n            _use_total_holding_mock.assert_called_once_with(\"order_type\")\n\n    with mock.patch.object(position_size, \"get_target_position\",\n                           mock.AsyncMock(return_value=(decimal.Decimal(10), \"buy\"))) as get_target_position_mock:\n        with mock.patch.object(create_order, \"_use_total_holding\",\n                               mock.Mock(return_value=True)) as _use_total_holding_mock, \\\n             mock.patch.object(create_order, \"_is_stop_order\",\n                               mock.Mock(return_value=False)) as _is_stop_order_mock:\n            assert await create_order._get_order_quantity_and_side(null_context, None, 1, \"order_type\", None, True,\n                                                                   False) \\\n                   == (decimal.Decimal(10), \"buy\")\n            get_target_position_mock.assert_called_once_with(null_context, 1, True, False, use_total_holding=True,\n                                                             unknown_portfolio_on_creation=False)\n            get_target_position_mock.reset_mock()\n            _is_stop_order_mock.assert_called_once_with(\"order_type\")\n            _use_total_holding_mock.assert_called_once_with(\"order_type\")\n        with mock.patch.object(create_order, \"_use_total_holding\",\n                               mock.Mock(return_value=False)) as _use_total_holding_mock, \\\n             mock.patch.object(create_order, \"_is_stop_order\",\n                               mock.Mock(return_value=True)) as _is_stop_order_mock:\n            assert await create_order._get_order_quantity_and_side(null_context, None, 1, \"order_type\", None, False,\n                                                                   True) \\\n                   == (decimal.Decimal(10), \"buy\")\n            get_target_position_mock.assert_called_once_with(null_context, 1, False, True, use_total_holding=False,\n                                                             unknown_portfolio_on_creation=True)\n            get_target_position_mock.reset_mock()\n            _is_stop_order_mock.assert_called_once_with(\"order_type\")\n            _use_total_holding_mock.assert_called_once_with(\"order_type\")\n\n\nasync def test_get_order_details(null_context):\n    ten = decimal.Decimal(10)\n    with mock.patch.object(script_keywords, \"get_price_with_offset\", mock.AsyncMock(return_value=ten)) as get_offset_mock:\n\n        async def _test_market(side, expected_order_type):\n            order_type, order_price, side, _, _, _, _, _, _ = await create_order._get_order_details(\n                null_context, \"market\", side, None, None, None\n            )\n            assert order_type is expected_order_type\n            assert order_price == ten\n            assert side is None\n            get_offset_mock.assert_called_once_with(null_context, \"0\")\n            get_offset_mock.reset_mock()\n        await _test_market(trading_enums.TradeOrderSide.SELL.value, trading_enums.TraderOrderType.SELL_MARKET)\n        await _test_market(trading_enums.TradeOrderSide.BUY.value, trading_enums.TraderOrderType.BUY_MARKET)\n\n        async def _test_limit(side, expected_order_type):\n            order_type, order_price, side, _, _, _, _, _, _ = await create_order._get_order_details(\n                null_context, \"limit\", side, \"25%\", None, None\n            )\n            assert order_type is expected_order_type\n            assert order_price == ten\n            assert side is None\n            get_offset_mock.assert_called_once_with(null_context, \"25%\")\n            get_offset_mock.reset_mock()\n        await _test_limit(trading_enums.TradeOrderSide.SELL.value, trading_enums.TraderOrderType.SELL_LIMIT)\n        await _test_limit(trading_enums.TradeOrderSide.BUY.value, trading_enums.TraderOrderType.BUY_LIMIT)\n\n        async def _test_stop_loss(side, expected_side):\n            order_type, order_price, side, _, _, _, _, _, _ = await create_order._get_order_details(\n                null_context, \"stop_loss\", side, \"25%\", None, None\n            )\n            assert order_type is trading_enums.TraderOrderType.STOP_LOSS\n            assert order_price == ten\n            assert side is expected_side\n            get_offset_mock.assert_called_once_with(null_context, \"25%\")\n            get_offset_mock.reset_mock()\n        await _test_stop_loss(trading_enums.TradeOrderSide.SELL.value, trading_enums.TradeOrderSide.SELL)\n        await _test_stop_loss(trading_enums.TradeOrderSide.BUY.value, trading_enums.TradeOrderSide.BUY)\n\n        async def _test_trailing_market(side, expected_side):\n            order_type, order_price, side, _, trailing_method, _, _, _, _ = await create_order._get_order_details(\n                null_context, \"trailing_market\", side, \"25%\", None, None\n            )\n            assert order_type is trading_enums.TraderOrderType.TRAILING_STOP\n            assert trailing_method == \"continuous\"\n            assert order_price == ten\n            assert side is expected_side\n            get_offset_mock.assert_called_once_with(null_context, \"25%\")\n            get_offset_mock.reset_mock()\n        await _test_trailing_market(trading_enums.TradeOrderSide.SELL.value, trading_enums.TradeOrderSide.SELL)\n        await _test_trailing_market(trading_enums.TradeOrderSide.BUY.value, trading_enums.TradeOrderSide.BUY)\n\n        async def _test_trailing_limit(side, expected_side):\n            order_type, order_price, side, _, trailing_method, min_offset_val, max_offset_val, _, _ \\\n                = await create_order._get_order_details(\n                null_context, \"trailing_limit\", side, \"25%\", None, None\n            )\n            assert order_type is trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            assert trailing_method == \"continuous\"\n            assert order_price is None\n            assert side is expected_side\n            assert min_offset_val == ten\n            assert max_offset_val == ten\n            assert get_offset_mock.call_count == 2\n            get_offset_mock.reset_mock()\n        await _test_trailing_limit(trading_enums.TradeOrderSide.SELL.value, trading_enums.TradeOrderSide.SELL)\n        await _test_trailing_limit(trading_enums.TradeOrderSide.BUY.value, trading_enums.TradeOrderSide.BUY)\n\n\nasync def test_create_order(mock_context, symbol_market):\n    with mock.patch.object(trading_personal_data, \"get_pre_order_data\",\n                           mock.AsyncMock(return_value=(None, None, decimal.Decimal(5), decimal.Decimal(105),\n                                                        symbol_market))) \\\n        as get_pre_order_data_mock, \\\n         mock.patch.object(create_order, \"_get_group_adapted_quantity\", mock.Mock(return_value=decimal.Decimal(1))) \\\n            as _get_group_adapted_quantity_mock:\n\n        # without linked orders\n        # don't plot orders\n        mock_context.plot_orders = False\n        orders = await create_order._create_order(\n            mock_context, \"BTC/USDT\", decimal.Decimal(1), decimal.Decimal(100), \"tag\",\n            \"order_type_name\", \"input_side\", trading_enums.TradeOrderSide.BUY.value, None,\n            trading_enums.TraderOrderType.BUY_MARKET, None, None, False, None, None,\n            None, None, None, None,\n            None, None, None, None,\n            None, True, None)\n        assert get_pre_order_data_mock.call_count == 2\n        _get_group_adapted_quantity_mock.assert_called_once_with(mock_context, None,\n                                                                 trading_enums.TraderOrderType.BUY_MARKET,\n                                                                 decimal.Decimal(1))\n        assert len(orders) == 1\n        assert isinstance(orders[0], trading_personal_data.BuyMarketOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].tag == \"tag\"\n        assert orders[0].origin_price == decimal.Decimal(105)\n        assert orders[0].origin_quantity == decimal.Decimal(1)\n        assert mock_context.just_created_orders == orders\n        mock_context.just_created_orders = []\n        get_pre_order_data_mock.reset_mock()\n        _get_group_adapted_quantity_mock.reset_mock()\n\n        # with order group\n        # plot orders\n        mock_context.plot_orders = True\n        oco_group = grouping.create_one_cancels_the_other_group(mock_context)\n        orders = await create_order._create_order(\n            mock_context, \"BTC/USDT\", decimal.Decimal(1), decimal.Decimal(100), \"tag2\",\n            \"order_type_name\", \"input_side\", trading_enums.TradeOrderSide.BUY.value, None,\n            trading_enums.TraderOrderType.TRAILING_STOP, decimal.Decimal(5), None, False, oco_group,\n            None, None, None, None,\n            None, None, None, None,\n            None, True, None, None)\n        get_pre_order_data_mock.assert_called_once_with(mock_context.exchange_manager, symbol=\"BTC/USDT\",\n                                                        timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n        _get_group_adapted_quantity_mock.assert_called_once_with(mock_context, oco_group,\n                                                                 trading_enums.TraderOrderType.TRAILING_STOP,\n                                                                 decimal.Decimal(1))\n        assert len(orders) == 1\n        assert isinstance(orders[0], trading_personal_data.TrailingStopOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].tag == \"tag2\"\n        assert orders[0].origin_price == decimal.Decimal(100)\n        assert orders[0].origin_quantity == decimal.Decimal(1)\n        assert orders[0].trader == mock_context.trader\n        assert orders[0].trailing_percent == decimal.Decimal(5)\n        assert orders[0].order_group is oco_group\n        assert mock_context.just_created_orders == orders\n        mock_context.just_created_orders = []\n        get_pre_order_data_mock.reset_mock()\n        _get_group_adapted_quantity_mock.reset_mock()\n\n        # with same order group as one previously created order: group them together\n        oco_group = grouping.create_one_cancels_the_other_group(mock_context)\n        previous_orders = [trading_personal_data.LimitOrder(mock_context.trader),\n                           trading_personal_data.LimitOrder(mock_context.trader)]\n        previous_orders[0].add_to_order_group(oco_group)\n        # with mock.patch.object(create_order, \"pre_initialize_order_callback\", mock.AsyncMock()) \\\n        #      as pre_initialize_order_callback_mock:\n        mock_context.plot_orders = False\n        orders = await create_order._create_order(\n            mock_context, \"BTC/USDT\", decimal.Decimal(1), decimal.Decimal(100), \"tag2\",\n            \"order_type_name\", \"side\", trading_enums.TradeOrderSide.BUY.value, trading_enums.TradeOrderSide.BUY,\n            trading_enums.TraderOrderType.TRAILING_STOP,\n            decimal.Decimal(5), None, True, oco_group,\n            None, None, None, None,\n            None, None, None, None,\n            None, True, None, None)\n        get_pre_order_data_mock.assert_called_once_with(mock_context.exchange_manager, symbol=\"BTC/USDT\",\n                                                        timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n        _get_group_adapted_quantity_mock.assert_called_once_with(mock_context, oco_group,\n                                                                 trading_enums.TraderOrderType.TRAILING_STOP,\n                                                                 decimal.Decimal(1))\n        assert len(orders) == 1\n        assert isinstance(orders[0], trading_personal_data.TrailingStopOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].tag == \"tag2\"\n        assert orders[0].origin_price == decimal.Decimal(100)\n        assert orders[0].origin_quantity == decimal.Decimal(1)\n        assert orders[0].trader == mock_context.trader\n        assert orders[0].trailing_percent == decimal.Decimal(5)\n        assert orders[0].order_group is oco_group\n        assert orders[0].side is trading_enums.TradeOrderSide.BUY\n        assert mock_context.just_created_orders == orders\n        mock_context.just_created_orders = []\n\n        grouped_orders = grouping.get_open_orders_from_group(oco_group)\n        assert len(grouped_orders) == 1  # only order this order got created and therefore is open in group\n        assert grouped_orders[0] is orders[0]\n\n\nasync def test_get_group_adapted_quantity(mock_context, skip_if_octobot_trading_mocking_disabled):\n    # skip_if_octobot_trading_mocking_disabled btps_group, \"can_create_order\"\n    oco_group = grouping.create_one_cancels_the_other_group(mock_context)\n    # no filter on oco groups\n    assert create_order._get_group_adapted_quantity(mock_context, oco_group, \"whatever\", decimal.Decimal(1000000)) \\\n           == decimal.Decimal(1000000)\n\n    btps_group = grouping.create_balanced_take_profit_and_stop_group(mock_context)\n    if os.getenv('CYTHON_IGNORE'):\n        return\n    with mock.patch.object(btps_group, \"can_create_order\", mock.Mock(return_value=False)) as can_create_order_mock, \\\n         mock.patch.object(btps_group, \"get_max_order_quantity\", mock.Mock(return_value=decimal.Decimal(1))) \\\n            as get_max_order_quantity_mock:\n        # no context.just_created_orders: never block 1st orders to create as they can't be balanced\n        assert create_order._get_group_adapted_quantity(mock_context, btps_group, \"whatever\", decimal.Decimal(100)) \\\n               == decimal.Decimal(100)\n        can_create_order_mock.assert_not_called()\n        get_max_order_quantity_mock.assert_not_called()\n\n        order_1 = mock.Mock(order_group=oco_group, order_type=trading_enums.TraderOrderType.STOP_LOSS)\n        mock_context.just_created_orders.append(order_1)\n        # context.just_created_orders has orders from other groups: consider this one as 1st from the group\n        assert create_order._get_group_adapted_quantity(mock_context, btps_group, \"whatever\", decimal.Decimal(100)) \\\n               == decimal.Decimal(100)\n        can_create_order_mock.assert_not_called()\n        get_max_order_quantity_mock.assert_not_called()\n\n        order_2 = mock.Mock(order_group=btps_group, order_type=trading_enums.TraderOrderType.SELL_LIMIT)\n        mock_context.just_created_orders.append(order_2)\n        # only take profits being created: allow it\n        assert create_order._get_group_adapted_quantity(mock_context, btps_group,\n                                                        trading_enums.TraderOrderType.SELL_LIMIT,\n                                                        decimal.Decimal(10)) \\\n               == decimal.Decimal(10)\n        can_create_order_mock.assert_not_called()\n        get_max_order_quantity_mock.assert_not_called()\n\n        # imbalanced orders: call can_create_order to figure out if we can create this order\n        assert create_order._get_group_adapted_quantity(mock_context, btps_group,\n                                                        trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                                                        decimal.Decimal(10)) == decimal.Decimal(1)\n        can_create_order_mock.assert_called_once_with(trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                                                      decimal.Decimal(10))\n        get_max_order_quantity_mock.assert_called_once_with(trading_enums.TraderOrderType.STOP_LOSS_LIMIT)\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_limit_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.limit_order as limit_order\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_limit(null_context):\n    with mock.patch.object(create_order, \"create_order_instance\", mock.AsyncMock()) as create_order_instance:\n        await limit_order.limit(null_context, \"side\", \"symbol\", \"amount\", \"target_position\", \"offset\",\n                                \"stop_loss_offset\", \"stop_loss_tag\", \"stop_loss_type\", \"stop_loss_group\",\n                                \"take_profit_offset\", \"take_profit_tag\", \"take_profit_type\", \"take_profit_group\",\n                                \"slippage_limit\", \"time_limit\", \"reduce_only\", \"post_only\", \"tag\", \"group\", \"wait_for\")\n        create_order_instance.assert_called_once_with(\n            null_context, side=\"side\", symbol=\"symbol\", order_amount=\"amount\", order_target_position=\"target_position\",\n            stop_loss_offset=\"stop_loss_offset\", stop_loss_tag=\"stop_loss_tag\", stop_loss_type=\"stop_loss_type\",\n            stop_loss_group=\"stop_loss_group\",\n            take_profit_offset=\"take_profit_offset\", take_profit_tag=\"take_profit_tag\",\n            take_profit_type=\"take_profit_type\", take_profit_group=\"take_profit_group\",\n            order_type_name=\"limit\", order_offset=\"offset\", slippage_limit=\"slippage_limit\", time_limit=\"time_limit\",\n            reduce_only=\"reduce_only\", post_only=\"post_only\", tag=\"tag\", group=\"group\", wait_for=\"wait_for\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_market_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.market_order as market_order\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_market(null_context):\n    with mock.patch.object(create_order, \"create_order_instance\", mock.AsyncMock()) as create_order_instance:\n        await market_order.market(null_context, \"side\", \"symbol\", \"amount\", \"target_position\",\n                                  \"stop_loss_offset\", \"stop_loss_tag\", \"stop_loss_type\", \"stop_loss_group\",\n                                  \"take_profit_offset\", \"take_profit_tag\", \"take_profit_type\", \"take_profit_group\",\n                                  \"reduce_only\", \"tag\", \"group\", \"wait_for\")\n        create_order_instance.assert_called_once_with(\n            null_context, side=\"side\", symbol=\"symbol\", order_amount=\"amount\", order_target_position=\"target_position\",\n            stop_loss_offset=\"stop_loss_offset\", stop_loss_tag=\"stop_loss_tag\", stop_loss_type=\"stop_loss_type\",\n            stop_loss_group=\"stop_loss_group\",\n            take_profit_offset=\"take_profit_offset\", take_profit_tag=\"take_profit_tag\",\n            take_profit_type=\"take_profit_type\", take_profit_group=\"take_profit_group\",\n            order_type_name=\"market\", reduce_only=\"reduce_only\", tag=\"tag\", group=\"group\", wait_for=\"wait_for\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_multiple_orders_creation.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport pytest\nimport mock\nimport decimal\nimport contextlib\nimport os\n\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.personal_data.orders.order_util as order_util\nimport octobot_trading.api as api\nimport octobot_trading.errors as errors\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport tentacles.Meta.Keywords.scripting_library as scripting_library\n\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, mock_context, \\\n    skip_if_octobot_trading_mocking_disabled\nfrom tentacles.Meta.Keywords.scripting_library.tests.exchanges import backtesting_trader, backtesting_config, \\\n    backtesting_exchange_manager, fake_backtesting\nimport tentacles.Meta.Keywords.scripting_library.tests.test_utils.order_util as test_order_util\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_orders_with_invalid_values(mock_context, skip_if_octobot_trading_mocking_disabled):\n    # skip_if_octobot_trading_mocking_disabled mock_context.trader, \"create_order\"\n    initial_usdt_holdings, btc_price = await _usdt_trading_context(mock_context)\n\n    if os.getenv('CYTHON_IGNORE'):\n        return\n    with mock.patch.object(trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)), \\\n         mock.patch.object(order_util, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)), \\\n         mock.patch.object(mock_context.trader, \"create_order\", mock.AsyncMock()) as create_order_mock:\n\n        with pytest.raises(errors.InvalidArgumentError):\n            # no amount\n            await scripting_library.market(\n                mock_context,\n                side=\"buy\"\n            )\n            create_order_mock.assert_not_called()\n            create_order_mock.reset_mock()\n\n        with pytest.raises(errors.InvalidArgumentError):\n            # negative amount\n            await scripting_library.market(\n                mock_context,\n                amount=\"-1\",\n                side=\"buy\"\n            )\n            create_order_mock.assert_not_called()\n            create_order_mock.reset_mock()\n\n        with pytest.raises(errors.InvalidArgumentError):\n            # missing offset parameter\n            await scripting_library.limit(\n                mock_context,\n                target_position=\"20%\",\n                side=\"buy\"\n            )\n\n        with pytest.raises(errors.InvalidArgumentError):\n            # missing side parameter\n            await scripting_library.market(\n                mock_context,\n                amount=\"1\"\n            )\n\n        # orders without having enough funds\n        for amount, side in ((1, \"sell\"), (0.000000001, \"buy\")):\n            await scripting_library.market(\n                mock_context,\n                amount=amount,\n                side=side\n            )\n            create_order_mock.assert_not_called()\n            create_order_mock.reset_mock()\n            mock_context.orders_writer.log_many.assert_not_called()\n            mock_context.orders_writer.log_many.reset_mock()\n            mock_context.logger.warning.assert_called_once()\n            mock_context.logger.warning.reset_mock()\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_orders_amount_then_position_sequence(mock_context):\n    initial_usdt_holdings, btc_price = await _usdt_trading_context(mock_context)\n    mock_context.exchange_manager.is_future = True\n    api.load_pair_contract(\n        mock_context.exchange_manager,\n        api.create_default_future_contract(\n            mock_context.symbol, decimal.Decimal(1), trading_enums.FutureContractType.LINEAR_PERPETUAL,\n            trading_constants.DEFAULT_SYMBOL_POSITION_MODE\n        ).to_dict()\n    )\n\n    if os.getenv('CYTHON_IGNORE'):\n        return\n    with mock.patch.object(trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)), \\\n         mock.patch.object(order_util, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)):\n\n        # buy for 10% of the total portfolio value\n        orders = await scripting_library.market(\n            mock_context,\n            amount=\"10%\",\n            side=\"buy\"\n        )\n        btc_val = decimal.Decimal(10)   # 10.00\n        usdt_val = decimal.Decimal(45000)   # 45000.00\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n        # buy for 10% of the portfolio available value\n        orders = await scripting_library.limit(\n            mock_context,\n            amount=\"10%a\",\n            offset=\"0\",\n            side=\"buy\"\n        )\n        btc_val = btc_val + decimal.Decimal(str((45000 * decimal.Decimal(\"0.1\")) / 500))    # 19.0\n        usdt_val = usdt_val * decimal.Decimal(str(0.9))     # 40500.00\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n        # buy for for 10% of the current position value\n        orders = await scripting_library.market(\n            mock_context,\n            amount=\"10%p\",\n            side=\"buy\"\n        )\n        usdt_val = usdt_val - (btc_val * decimal.Decimal(\"0.1\") * btc_price)   # 39550.00\n        btc_val = btc_val * decimal.Decimal(\"1.1\")   # 20.90\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n    # price changes to 1000\n    btc_price = 1000\n    mock_context.exchange_manager.exchange_personal_data.portfolio_manager.handle_mark_price_update(\n        \"BTC/USDT\", btc_price)\n    with mock.patch.object(trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)), \\\n         mock.patch.object(order_util, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)):\n\n        # buy to reach a target position of 25 btc\n        orders = await scripting_library.market(\n            mock_context,\n            target_position=25\n        )\n        usdt_val = usdt_val - ((25 - btc_val) * btc_price)   # 35450.00\n        btc_val = decimal.Decimal(25)   # 25\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n        # buy to reach a target position of 60% of the total portfolio (in BTC)\n        orders = await scripting_library.limit(\n            mock_context,\n            target_position=\"60%\",\n            offset=0\n        )\n        previous_btc_val = btc_val\n        btc_val = (btc_val + (usdt_val / btc_price)) * decimal.Decimal(\"0.6\")   # 36.27\n        usdt_val = usdt_val - (btc_val - previous_btc_val) * btc_price   # 24180.00\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n        # buy to reach a target position including an additional 50% of the available USDT in BTC\n        orders = await scripting_library.market(\n            mock_context,\n            target_position=\"50%a\"\n        )\n        btc_val = btc_val + usdt_val / 2 / btc_price   # 48.36\n        usdt_val = usdt_val / 2   # 12090.00\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n        # sell to keep only 10% of the position, sell at 2000 (1000 + 100%)\n        orders = await scripting_library.limit(\n            mock_context,\n            target_position=\"10%p\",\n            offset=\"100%\"\n        )\n        usdt_val = usdt_val + btc_val * decimal.Decimal(\"0.9\") * (btc_price * 2)  # 99138.00\n        btc_val = btc_val / 10   # 4.836\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders)\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_concurrent_orders(mock_context):\n    async with _20_percent_position_trading_context(mock_context) as context_data:\n        btc_val, usdt_val, btc_price = context_data\n\n        # create 3 sell orders (at price = 500 + 10 = 510)\n        # that would end up selling more than what we have if not executed sequentially\n        # 1st order is 80% of available btc, second is 80% of the remaining 20% and so on\n\n        orders = []\n        async def create_order(amount):\n            orders.append(\n                (await scripting_library.limit(\n                    mock_context,\n                    amount=amount,\n                    offset=10,\n                    side=\"sell\"\n                ))[0]\n            )\n        await asyncio.gather(\n            *(\n                create_order(\"80%a\")\n                for _ in range(3)\n            )\n        )\n\n        initial_btc_holdings = btc_val\n        btc_val = initial_btc_holdings * (decimal.Decimal(\"0.2\") ** 3)\n        usdt_val = usdt_val + (initial_btc_holdings - btc_val) * (btc_price + 10)   # 50118.40\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders, orders_count=3)\n\n        # create 3 buy orders (at price = 500 + 10 = 510) all of them for a target position of 10%\n        # first order gets created to have this 10% position, others are also created like this, ending up in a 30%\n        # position\n\n        # update portfolio current value\n        mock_context.exchange_manager.exchange_personal_data.portfolio_manager.handle_balance_updated()\n\n        orders = []\n\n        async def create_order(target_position):\n            orders.append(\n                (await scripting_library.limit(\n                    mock_context,\n                    target_position=target_position,\n                    offset=10\n                ))[0]\n            )\n        await asyncio.gather(\n            *(\n                create_order(\"10%\")\n                for _ in range(3)\n            )\n        )\n\n        initial_btc_holdings = btc_val  # 0.16\n        initial_total_val = initial_btc_holdings * btc_price + usdt_val\n        initial_position_percent = decimal.Decimal(initial_btc_holdings * btc_price / initial_total_val)\n        btc_val = initial_btc_holdings + \\\n                  initial_total_val * (decimal.Decimal(\"0.1\") - initial_position_percent) * 3 / btc_price    # 29.79904\n        usdt_val = usdt_val - (btc_val - initial_btc_holdings) * (btc_price + 10)   # 35002.4896\n        await _fill_and_check(mock_context, btc_val, usdt_val, orders, orders_count=3)\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_sell_limit_with_stop_loss_orders_single_sell_and_stop_with_oco_group(mock_context):\n    async with _20_percent_position_trading_context(mock_context) as context_data:\n        btc_val, usdt_val, btc_price = context_data\n\n        mock_context.allow_artificial_orders = True  # make stop loss not lock funds\n        oco_group = scripting_library.create_one_cancels_the_other_group(mock_context)\n        sell_limit_orders = await scripting_library.limit(\n            mock_context,\n            target_position=\"0%\",\n            offset=50,\n            group=oco_group\n        )\n        # add_to_order_group(oco_group, sell_limit_orders)\n        stop_loss_orders = await scripting_library.stop_loss(\n            mock_context,\n            target_position=\"0%\",\n            offset=-75,\n            group=oco_group\n        )\n        assert len(sell_limit_orders) == len(stop_loss_orders) == 1\n\n        # stop order is filled\n        usdt_val = usdt_val + btc_val * (btc_price - 75)   # 48500.00\n        btc_val = trading_constants.ZERO    # 0.00\n        await _fill_and_check(mock_context, btc_val, usdt_val, stop_loss_orders, logged_orders_count=2)\n        # linked order is cancelled\n        assert sell_limit_orders[0].is_cancelled()\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_sell_limit_with_stop_loss_orders_two_sells_and_stop_with_oco(mock_context):\n    async with _20_percent_position_trading_context(mock_context) as context_data:\n        btc_val, usdt_val, btc_price = context_data\n\n        mock_context.allow_artificial_orders = True  # make stop loss not lock funds\n        oco_group = scripting_library.create_one_cancels_the_other_group(mock_context)\n        stop_loss_orders = await scripting_library.stop_loss(\n            mock_context,\n            target_position=\"0%\",\n            offset=-50,\n            side=\"sell\",\n            group=oco_group,\n            tag=\"exitPosition\"\n        )\n        take_profit_limit_orders_1 = await scripting_library.limit(\n            mock_context,\n            target_position=\"50%p\",\n            offset=50\n        )\n        take_profit_limit_orders_2 = await scripting_library.limit(\n            mock_context,\n            target_position=\"0%p\",\n            offset=100,\n            group=oco_group,\n            tag=\"exitPosition\"\n        )\n\n        # take_profit_limit_orders_1 filled\n        available_btc_val = trading_constants.ZERO  # 10.00\n        total_btc_val = btc_val / 2  # 10.00\n        usdt_val = usdt_val + btc_val / 2 * (btc_price + 50)   # 40000.00\n        await _fill_and_check(mock_context, available_btc_val, usdt_val, take_profit_limit_orders_1,\n                              btc_total=total_btc_val)\n        # linked order is not cancelled\n        assert stop_loss_orders[0].is_open()\n\n        # take_profit_limit_orders_2 filled\n        usdt_val = usdt_val + btc_val / 2 * (btc_price + 100)   # 40000.00\n        btc_val = trading_constants.ZERO  # 0.00\n        await _fill_and_check(mock_context, btc_val, usdt_val, take_profit_limit_orders_2)\n        # linked order is cancelled\n        assert stop_loss_orders[0].is_cancelled()\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_sell_limit_with_multiple_stop_loss_and_sell_orders_in_balanced_take_profit_and_stop_group(mock_context):\n    async with _20_percent_position_trading_context(mock_context) as context_data:\n        btc_val, usdt_val, btc_price = context_data\n\n        mock_context.allow_artificial_orders = True  # make stop loss not lock funds\n        btsl_group_1 = scripting_library.create_balanced_take_profit_and_stop_group(mock_context)\n        g1_stop_1 = await scripting_library.stop_loss(\n            mock_context, amount=\"2\", offset=-50, side=\"sell\", group=btsl_group_1, tag=\"exitPosition1\"\n        )\n        g1_stop_2 = await scripting_library.stop_loss(\n            mock_context, amount=\"3\", offset=-100, side=\"sell\", group=btsl_group_1, tag=\"exitPosition1\"\n        )\n        g1_stop_3 = await scripting_library.stop_loss(\n            mock_context, amount=\"4\", offset=-150, side=\"sell\", group=btsl_group_1, tag=\"exitPosition1\"\n        )\n        g1_tp_1 = await scripting_library.limit(\n            mock_context, amount=\"4\", offset=50, side=\"sell\", group=btsl_group_1, tag=\"exitPosition1\"\n        )\n        g1_tp_2 = await scripting_library.limit(\n            mock_context, amount=\"5\", offset=100, side=\"sell\", group=btsl_group_1, tag=\"exitPosition1\"\n        )\n\n        btsl_group_2 = scripting_library.create_balanced_take_profit_and_stop_group(mock_context)\n        g2_stop_1 = await scripting_library.stop_loss(\n            mock_context, amount=\"5\", offset=-50, side=\"sell\", group=btsl_group_2, tag=\"exitPosition1\"\n        )\n        g2_tp_1 = await scripting_library.limit(\n            mock_context, amount=\"3\", offset=50, side=\"sell\", group=btsl_group_2, tag=\"exitPosition1\"\n        )\n        g2_tp_2 = await scripting_library.limit(\n            mock_context, amount=\"2\", offset=100, side=\"sell\", group=btsl_group_2, tag=\"exitPosition1\"\n        )\n\n        # g1_tp_1 filled\n        available_btc_val = decimal.Decimal(6)\n        sold_btc = decimal.Decimal(4)\n        total_btc_val = btc_val - sold_btc\n        usdt_val = usdt_val + sold_btc * (btc_price + 50)\n        await _fill_and_check(mock_context, available_btc_val, usdt_val, g1_tp_1, btc_total=total_btc_val)\n        # g1_stop_3 is cancelled (same size), other are untouched\n        assert g1_stop_3[0].is_cancelled()\n        assert all(o[0].is_open() for o in [g1_stop_1, g1_stop_2, g1_tp_2, g2_stop_1, g2_tp_1, g2_tp_2])\n\n        # g1_stop_1 filled\n        sold_btc = decimal.Decimal(2)\n        total_btc_val = total_btc_val - sold_btc\n        usdt_val = usdt_val + sold_btc * (btc_price - 50)\n        await _fill_and_check(mock_context, available_btc_val, usdt_val, g1_stop_1, btc_total=total_btc_val)\n        # g1_tp_1 is edited (reduced size), other are untouched\n        assert g1_tp_2[0].origin_quantity == decimal.Decimal(3)  # 5 - 2\n        assert all(o[0].is_open() for o in [g1_stop_2, g1_tp_2, g2_stop_1, g2_tp_1, g2_tp_2])\n\n        # g2_stop_1 filled\n        sold_btc = decimal.Decimal(5)\n        total_btc_val = total_btc_val - sold_btc\n        usdt_val = usdt_val + sold_btc * (btc_price - 50)\n        await _fill_and_check(mock_context, available_btc_val, usdt_val, g2_stop_1, btc_total=total_btc_val)\n        # g1_tp_1 is edited (reduced size), other are untouched\n        assert all(o[0].is_cancelled() for o in [g2_tp_1, g2_tp_2])\n        assert all(o[0].is_open() for o in [g1_stop_2, g1_tp_2])\n\n        # g1_stop_2 cancelled\n        await mock_context.trader.cancel_order(g1_stop_2[0])\n        # g1_tp_2 is cancelled as well\n        assert all(o[0].is_cancelled() for o in [g1_stop_2, g1_tp_2])\n        assert scripting_library.get_open_orders(mock_context) == []\n\n\n@pytest.mark.parametrize(\"backtesting_config\", [\"USDT\"], indirect=[\"backtesting_config\"])\nasync def test_multiple_sell_limit_with_stop_loss_rounding_issues_in_balanced_take_profit_and_stop_group(mock_context):\n    async with _20_percent_position_trading_context(mock_context) as context_data:\n        btc_val, usdt_val, btc_price = context_data\n\n        mock_context.allow_artificial_orders = True  # make stop loss not lock funds\n        btsl_group_1 = scripting_library.create_balanced_take_profit_and_stop_group(mock_context)\n        # disable to create orders\n        await btsl_group_1.enable(False)\n        position_size = decimal.Decimal(20)\n        added_amount = decimal.Decimal(\"0.00100001111\")\n\n        market_1 = await scripting_library.market(mock_context, amount=added_amount, side=\"buy\")\n        assert market_1[0].is_filled()\n        amount = position_size + decimal.Decimal(\"0.00100001\")  # ending \"111\" got truncated\n        assert api.get_portfolio_currency(mock_context.exchange_manager, \"BTC\").total == amount\n        assert api.get_portfolio_currency(mock_context.exchange_manager, \"BTC\").available == amount\n\n        g1_stop_1 = await scripting_library.stop_loss(\n            mock_context, amount=amount, offset=-50, side=\"sell\", group=btsl_group_1, tag=\"exitPosition1\"\n        )\n        g1_tp_1 = await scripting_library.limit(\n            mock_context, amount=amount * decimal.Decimal(\"0.5\"), offset=50, side=\"sell\", group=btsl_group_1,\n            reduce_only=True\n        )\n        g1_tp_2 = await scripting_library.limit(\n            mock_context, amount=amount * decimal.Decimal(\"0.5\"), offset=100, side=\"sell\", group=btsl_group_1,\n            reduce_only=True\n        )\n\n        assert g1_stop_1[0].origin_quantity == amount\n        assert g1_tp_1[0].origin_quantity == decimal.Decimal('10.00050001')\n        assert g1_tp_2[0].origin_quantity == decimal.Decimal('10.00050000')\n\n        # enable order group: no order edit is triggered as scripting_library took care of the rounding issue of\n        # 20.00100001 / 2\n        await btsl_group_1.enable(False)\n\n        assert g1_stop_1[0].origin_quantity == amount\n        assert g1_tp_1[0].origin_quantity == decimal.Decimal('10.00050001')\n        assert g1_tp_2[0].origin_quantity == decimal.Decimal('10.00050000')\n\n\nasync def _usdt_trading_context(mock_context):\n    initial_usdt_holdings = 50000\n    mock_context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.update_portfolio_from_balance({\n        'BTC': {'available': decimal.Decimal(0), 'total': decimal.Decimal(0)},\n        'ETH': {'available': decimal.Decimal(0), 'total': decimal.Decimal(0)},\n        'USDT': {'available': decimal.Decimal(str(initial_usdt_holdings)),\n                 'total': decimal.Decimal(str(initial_usdt_holdings))}\n    }, mock_context.exchange_manager)\n    mock_context.exchange_manager.exchange_personal_data.portfolio_manager.handle_balance_updated()\n    btc_price = 500\n    mock_context.exchange_manager.exchange_personal_data.portfolio_manager.handle_mark_price_update(\n        \"BTC/USDT\", btc_price)\n    return initial_usdt_holdings, btc_price\n\n\n@contextlib.asynccontextmanager\nasync def _20_percent_position_trading_context(mock_context):\n    initial_usdt_holdings, btc_price = await _usdt_trading_context(mock_context)\n    usdt_val = decimal.Decimal(str(initial_usdt_holdings))\n    with mock.patch.object(trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)), \\\n            mock.patch.object(order_util, \"get_up_to_date_price\", mock.AsyncMock(return_value=btc_price)):\n        # initial limit buy order: buy with 20% of portfolio\n        buy_limit_orders = await scripting_library.limit(\n            mock_context,\n            target_position=\"20%\",\n            offset=0,\n            side=\"buy\"\n        )\n        btc_val = (usdt_val * decimal.Decimal(\"0.2\")) / btc_price  # 20.00\n        usdt_val = usdt_val * decimal.Decimal(\"0.8\")  # 40000.00\n        # position size = 20 BTC\n        await _fill_and_check(mock_context, btc_val, usdt_val, buy_limit_orders)\n        yield btc_val, usdt_val, btc_price\n\n\nasync def _fill_and_check(mock_context, btc_available, usdt_available, orders,\n                          btc_total=None, usdt_total=None, orders_count=1, logged_orders_count=None):\n    for order in orders:\n        if isinstance(order, trading_personal_data.LimitOrder):\n            await test_order_util.fill_limit_or_stop_order(order)\n        elif isinstance(order, trading_personal_data.MarketOrder):\n            await test_order_util.fill_market_order(order)\n\n    _ensure_orders_validity(mock_context, btc_available, usdt_available, orders,\n                            btc_total=btc_total, usdt_total=usdt_total, orders_count=orders_count,\n                            logged_orders_count=logged_orders_count)\n\n\ndef _ensure_orders_validity(mock_context, btc_available, usdt_available, orders,\n                            btc_total=None, usdt_total=None, orders_count=1, logged_orders_count=None):\n    exchange_manager = mock_context.exchange_manager\n    btc_total = btc_total or btc_available\n    usdt_total = usdt_total or usdt_available\n    assert len(orders) == orders_count\n    assert all(isinstance(order, trading_personal_data.Order) for order in orders)\n    assert mock_context.orders_writer.log_many.call_count == logged_orders_count or orders_count\n    mock_context.orders_writer.log_many.reset_mock()\n    mock_context.logger.warning.assert_not_called()\n    mock_context.logger.warning.reset_mock()\n    mock_context.logger.exception.assert_not_called()\n    mock_context.logger.exception.reset_mock()\n    assert api.get_portfolio_currency(exchange_manager, \"BTC\").available == btc_available\n    assert api.get_portfolio_currency(exchange_manager, \"BTC\").total == btc_total\n    assert api.get_portfolio_currency(exchange_manager, \"USDT\").available == usdt_available\n    assert api.get_portfolio_currency(exchange_manager, \"USDT\").total == usdt_total\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_stop_loss_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.stop_loss_order as stop_loss_order\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_stop_loss(null_context):\n    with mock.patch.object(create_order, \"create_order_instance\", mock.AsyncMock()) as create_order_instance:\n        await stop_loss_order.stop_loss(null_context, \"side\", \"symbol\", \"offset\", \"amount\", \"target_position\",\n                                        \"tag\", \"group\", \"wait_for\")\n        create_order_instance.assert_called_once_with(\n            null_context, side=\"side\", symbol=\"symbol\", order_amount=\"amount\", order_target_position=\"target_position\",\n            order_type_name=\"stop_loss\", order_offset=\"offset\", reduce_only=True,\n            tag=\"tag\", group=\"group\", wait_for=\"wait_for\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_trailing_limit_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.trailing_limit_order as trailing_limit_order\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_trailing_limit(null_context):\n    with mock.patch.object(create_order, \"create_order_instance\", mock.AsyncMock()) as create_order_instance:\n        await trailing_limit_order.trailing_limit(null_context, \"side\", \"symbol\", \"amount\", \"target_position\",\n                                                  \"offset\", \"min_offset\", \"max_offset\", \"slippage_limit\", \"time_limit\",\n                                                  \"reduce_only\", \"post_only\",\n                                                  \"tag\", \"group\", \"wait_for\")\n        create_order_instance.assert_called_once_with(\n            null_context, side=\"side\", symbol=\"symbol\", order_amount=\"amount\", order_target_position=\"target_position\",\n            order_type_name=\"trailing_limit\", order_min_offset=\"min_offset\", order_max_offset=\"max_offset\",\n            order_offset=\"offset\", slippage_limit=\"slippage_limit\", time_limit=\"time_limit\", reduce_only=\"reduce_only\",\n            post_only=\"post_only\", tag=\"tag\", group=\"group\", wait_for=\"wait_for\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_trailing_market_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.trailing_market_order as trailing_market_order\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_trailing_market(null_context):\n    with mock.patch.object(create_order, \"create_order_instance\", mock.AsyncMock()) as create_order_instance:\n        await trailing_market_order.trailing_market(null_context, \"side\", \"symbol\", \"amount\", \"target_position\",\n                                                    \"offset\", \"reduce_only\", \"tag\", \"group\", \"wait_for\")\n        create_order_instance.assert_called_once_with(\n            null_context, side=\"side\", symbol=\"symbol\", order_amount=\"amount\", order_target_position=\"target_position\",\n            order_type_name=\"trailing_market\", order_offset=\"offset\", reduce_only=\"reduce_only\",\n            tag=\"tag\", group=\"group\", wait_for=\"wait_for\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/order_types/test_trailing_stop_loss_order.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.create_order as create_order\nimport tentacles.Meta.Keywords.scripting_library.orders.order_types.trailing_stop_loss_order as trailing_stop_loss_order\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, null_context\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_trailing_stop_loss(null_context):\n    with mock.patch.object(create_order, \"create_order_instance\", mock.AsyncMock()) as create_order_instance:\n        await trailing_stop_loss_order.trailing_stop_loss(null_context, \"side\", \"symbol\", \"amount\", \"target_position\",\n                                                          \"offset\", \"reduce_only\", \"tag\", \"group\", \"wait_for\")\n        create_order_instance.assert_called_once_with(\n            null_context, side=\"side\", symbol=\"symbol\", order_amount=\"amount\", order_target_position=\"target_position\",\n            order_type_name=\"trailing_stop_loss\", order_offset=\"offset\", reduce_only=\"reduce_only\",\n            tag=\"tag\", group=\"group\", wait_for=\"wait_for\")\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/position_size/__init__.py",
    "content": "# Copyright\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/position_size/test_target_position.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport mock\nimport decimal\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as errors\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Meta.Keywords.scripting_library.orders.position_size.target_position as target_position\nimport tentacles.Meta.Keywords.scripting_library.data.reading.exchange_private_data as exchange_private_data\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, mock_context\nfrom tentacles.Meta.Keywords.scripting_library.tests.exchanges import backtesting_trader, backtesting_config, \\\n    backtesting_exchange_manager, fake_backtesting\n\n\ndef test_get_target_position_side():\n    assert target_position.get_target_position_side(1) == trading_enums.TradeOrderSide.BUY.value\n    assert target_position.get_target_position_side(-1) == trading_enums.TradeOrderSide.SELL.value\n    with pytest.raises(RuntimeError):\n        target_position.get_target_position_side(0)\n\n\n@pytest.mark.asyncio\nasync def test_get_target_position(mock_context):\n    with pytest.raises(errors.InvalidArgumentError):\n        await target_position.get_target_position(mock_context, \"1sdsqdq\")\n\n    # with positive (long) position\n    with mock.patch.object(script_keywords, \"adapt_amount_to_holdings\",\n                           mock.AsyncMock(return_value=decimal.Decimal(1))) as adapt_amount_to_holdings_mock, \\\n         mock.patch.object(exchange_private_data, \"open_position_size\",\n                              mock.Mock(return_value=decimal.Decimal(10))) as open_position_size_mock:\n\n        with mock.patch.object(script_keywords, \"parse_quantity\",\n                               mock.Mock(return_value=(script_keywords.QuantityType.POSITION_PERCENT, decimal.Decimal(10)))) \\\n                as parse_quantity_mock, \\\n             mock.patch.object(target_position, \"get_target_position_side\",\n                               mock.Mock(return_value=trading_enums.TradeOrderSide.SELL.value)) \\\n                as get_target_position_side_mock:\n            assert await target_position.get_target_position(mock_context, \"1\", target_price=\"hello\") == \\\n                   (decimal.Decimal(1), trading_enums.TradeOrderSide.SELL.value)\n            parse_quantity_mock.assert_called_once_with(\"1\")\n            open_position_size_mock.assert_called_once_with(mock_context)\n            get_target_position_side_mock.assert_called_once_with(decimal.Decimal(-9))\n            adapt_amount_to_holdings_mock.assert_called_once_with(mock_context, decimal.Decimal(9),\n                                                                  trading_enums.TradeOrderSide.SELL.value,\n                                                                  False, True, False, target_price=\"hello\")\n            adapt_amount_to_holdings_mock.reset_mock()\n            get_target_position_side_mock.reset_mock()\n            open_position_size_mock.reset_mock()\n\n        with mock.patch.object(script_keywords, \"parse_quantity\",\n                               mock.Mock(return_value=(script_keywords.QuantityType.PERCENT, decimal.Decimal(110)))) \\\n                as parse_quantity_mock, \\\n             mock.patch.object(script_keywords, \"total_account_balance\",\n                               mock.AsyncMock(return_value=decimal.Decimal(10))) \\\n                as total_account_balance_mock, \\\n             mock.patch.object(target_position, \"get_target_position_side\",\n                               mock.Mock(return_value=trading_enums.TradeOrderSide.BUY.value)) \\\n                as get_target_position_side_mock:\n            assert await target_position.get_target_position(mock_context, \"1\", use_total_holding=True,\n                                                             reduce_only=False, is_stop_order=True) == \\\n                   (decimal.Decimal(1), trading_enums.TradeOrderSide.BUY.value)\n            parse_quantity_mock.assert_called_once_with(\"1\")\n            total_account_balance_mock.assert_called_once_with(mock_context)\n            open_position_size_mock.assert_called_once_with(mock_context)\n            get_target_position_side_mock.assert_called_once_with(decimal.Decimal(1))\n            adapt_amount_to_holdings_mock.assert_called_once_with(mock_context, decimal.Decimal(1),\n                                                                  trading_enums.TradeOrderSide.BUY.value,\n                                                                  True, False, True, target_price=None)\n            adapt_amount_to_holdings_mock.reset_mock()\n            get_target_position_side_mock.reset_mock()\n            open_position_size_mock.reset_mock()\n\n        with mock.patch.object(script_keywords, \"parse_quantity\",\n                               mock.Mock(return_value=(script_keywords.QuantityType.FLAT, decimal.Decimal(-3)))) \\\n                as parse_quantity_mock, \\\n             mock.patch.object(target_position, \"get_target_position_side\",\n                               mock.Mock(return_value=trading_enums.TradeOrderSide.SELL.value)) \\\n                as get_target_position_side_mock:\n            assert await target_position.get_target_position(mock_context, \"1\") == \\\n                   (decimal.Decimal(1), trading_enums.TradeOrderSide.SELL.value)\n            parse_quantity_mock.assert_called_once_with(\"1\")\n            open_position_size_mock.assert_called_once_with(mock_context)\n            get_target_position_side_mock.assert_called_once_with(decimal.Decimal(-13))\n            adapt_amount_to_holdings_mock.assert_called_once_with(mock_context, decimal.Decimal(13),\n                                                                  trading_enums.TradeOrderSide.SELL.value,\n                                                                  False, True, False, target_price=None)\n            adapt_amount_to_holdings_mock.reset_mock()\n            get_target_position_side_mock.reset_mock()\n            open_position_size_mock.reset_mock()\n\n        with mock.patch.object(script_keywords, \"parse_quantity\",\n                               mock.Mock(return_value=(script_keywords.QuantityType.AVAILABLE_PERCENT, decimal.Decimal(25)))) \\\n                as parse_quantity_mock, \\\n             mock.patch.object(script_keywords, \"available_account_balance\",\n                               mock.AsyncMock(return_value=decimal.Decimal(5))) \\\n                as available_account_balance_mock, \\\n             mock.patch.object(target_position, \"get_target_position_side\",\n                               mock.Mock(return_value=trading_enums.TradeOrderSide.BUY.value)) \\\n                as get_target_position_side_mock:\n            assert await target_position.get_target_position(mock_context, \"1\") == \\\n                   (decimal.Decimal(1), trading_enums.TradeOrderSide.BUY.value)\n            parse_quantity_mock.assert_called_once_with(\"1\")\n            available_account_balance_mock.assert_called_once_with(mock_context, reduce_only=True)\n            # we are at initially at 10, we want add 20% of 5 => need to buy 1.25\n            get_target_position_side_mock.assert_called_once_with(decimal.Decimal(\"1.25\"))\n            adapt_amount_to_holdings_mock.assert_called_once_with(mock_context, decimal.Decimal(1.25),\n                                                                  trading_enums.TradeOrderSide.BUY.value,\n                                                                  False, True, False, target_price=None)\n            adapt_amount_to_holdings_mock.reset_mock()\n            get_target_position_side_mock.reset_mock()\n            open_position_size_mock.reset_mock()\n\n    # with negative (short) position\n    with mock.patch.object(script_keywords, \"adapt_amount_to_holdings\",\n                           mock.AsyncMock(return_value=decimal.Decimal(2))) as adapt_amount_to_holdings_mock, \\\n        mock.patch.object(exchange_private_data, \"open_position_size\",\n                          mock.Mock(return_value=decimal.Decimal(-10))) as open_position_size_mock:\n        with mock.patch.object(script_keywords, \"parse_quantity\",\n                               mock.Mock(return_value=(script_keywords.QuantityType.DELTA, decimal.Decimal(-3)))) \\\n                as parse_quantity_mock, \\\n             mock.patch.object(target_position, \"get_target_position_side\",\n                               mock.Mock(return_value=trading_enums.TradeOrderSide.BUY.value)) \\\n                as get_target_position_side_mock:\n            assert await target_position.get_target_position(mock_context, \"1\") == \\\n                   (decimal.Decimal(2), trading_enums.TradeOrderSide.BUY.value)\n            parse_quantity_mock.assert_called_once_with(\"1\")\n            open_position_size_mock.assert_called_once_with(mock_context)\n            get_target_position_side_mock.assert_called_once_with(decimal.Decimal(7))\n            adapt_amount_to_holdings_mock.assert_called_once_with(mock_context, decimal.Decimal(7),\n                                                                  trading_enums.TradeOrderSide.BUY.value,\n                                                                  False, True, False, target_price=None)\n            adapt_amount_to_holdings_mock.reset_mock()\n            get_target_position_side_mock.reset_mock()\n            open_position_size_mock.reset_mock()\n\n        with mock.patch.object(script_keywords, \"parse_quantity\",\n                               mock.Mock(return_value=(script_keywords.QuantityType.POSITION_PERCENT, decimal.Decimal(-3)))) \\\n                as parse_quantity_mock, \\\n             mock.patch.object(target_position, \"get_target_position_side\",\n                               mock.Mock(return_value=trading_enums.TradeOrderSide.BUY.value)) \\\n                as get_target_position_side_mock:\n            assert await target_position.get_target_position(mock_context, \"1\") == \\\n                   (decimal.Decimal(2), trading_enums.TradeOrderSide.BUY.value)\n            parse_quantity_mock.assert_called_once_with(\"1\")\n            open_position_size_mock.assert_called_once_with(mock_context)\n            get_target_position_side_mock.assert_called_once_with(decimal.Decimal(\"10.3\"))\n            adapt_amount_to_holdings_mock.assert_called_once_with(mock_context, decimal.Decimal(\"10.3\"),\n                                                                  trading_enums.TradeOrderSide.BUY.value,\n                                                                  False, True, False, target_price=None)\n            adapt_amount_to_holdings_mock.reset_mock()\n            get_target_position_side_mock.reset_mock()\n            open_position_size_mock.reset_mock()\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/orders/test_cancelling.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\nimport mock\n\nimport tentacles.Meta.Keywords.scripting_library.orders.order_tags as order_tags\nimport tentacles.Meta.Keywords.scripting_library.orders.cancelling as cancelling\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\n\nfrom tentacles.Meta.Keywords.scripting_library.tests import event_loop, mock_context, \\\n    skip_if_octobot_trading_mocking_disabled\nfrom tentacles.Meta.Keywords.scripting_library.tests.exchanges import backtesting_trader, backtesting_config, \\\n    backtesting_exchange_manager, fake_backtesting\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_cancel_orders(mock_context, skip_if_octobot_trading_mocking_disabled):\n    # skip_if_octobot_trading_mocking_disabled mock_context.trader, \"cancel_order\"\n    tagged_orders = [\"order_1\", \"order_2\"]\n    with mock.patch.object(mock_context.trader, \"cancel_order\",\n                           mock.AsyncMock(return_value=True)) as cancel_order_mock, \\\n         mock.patch.object(mock_context.trader, \"cancel_open_orders\", mock.AsyncMock(return_value=(True, []))) \\\n            as cancel_open_orders_mock:\n        with mock.patch.object(order_tags, \"get_tagged_orders\", mock.Mock(return_value=tagged_orders)) \\\n                as get_tagged_orders_mock:\n            # cancel all orders from context symbol\n            assert await cancelling.cancel_orders(mock_context) is True\n            get_tagged_orders_mock.assert_not_called()\n            cancel_order_mock.assert_not_called()\n            cancel_open_orders_mock.assert_called_once_with(\n                mock_context.symbol, cancel_loaded_orders=True, side=None,\n                since=trading_constants.NO_DATA_LIMIT,\n                until=trading_constants.NO_DATA_LIMIT\n            )\n            cancel_open_orders_mock.reset_mock()\n\n            # cancel sided orders from context symbol\n            side_str_to_side = {\n                \"sell\": trading_enums.TradeOrderSide.SELL,\n                \"buy\": trading_enums.TradeOrderSide.BUY,\n                \"all\": None,\n            }\n            for side, value in side_str_to_side.items():\n                assert await cancelling.cancel_orders(mock_context, which=side, cancel_loaded_orders=False) is True\n                get_tagged_orders_mock.assert_not_called()\n                cancel_order_mock.assert_not_called()\n                cancel_open_orders_mock.assert_called_once_with(\n                    mock_context.symbol, cancel_loaded_orders=False, side=value,\n                    since=trading_constants.NO_DATA_LIMIT,\n                    until=trading_constants.NO_DATA_LIMIT\n                )\n                cancel_open_orders_mock.reset_mock()\n\n            # different symbol values\n            assert await cancelling.cancel_orders(mock_context, symbol=\"ETH/USDT\") is True\n            get_tagged_orders_mock.assert_not_called()\n            cancel_order_mock.assert_not_called()\n            cancel_open_orders_mock.assert_called_once_with(\n                \"ETH/USDT\", cancel_loaded_orders=True, side=value,\n                since=trading_constants.NO_DATA_LIMIT,\n                until=trading_constants.NO_DATA_LIMIT\n            )\n            cancel_open_orders_mock.reset_mock()\n            assert await cancelling.cancel_orders(mock_context, symbols=[\"ETH/USDT\", \"USDT/USDC\"]) is True\n            get_tagged_orders_mock.assert_not_called()\n            cancel_order_mock.assert_not_called()\n            assert cancel_open_orders_mock.mock_calls[0].args == (\"ETH/USDT\", )\n            assert cancel_open_orders_mock.mock_calls[1].args == (\"USDT/USDC\", )\n            cancel_open_orders_mock.reset_mock()\n\n            # tags\n            assert await cancelling.cancel_orders(mock_context, which=\"tag1\") is True\n            get_tagged_orders_mock.assert_called_once_with(\n                mock_context, \"tag1\", symbol=None,\n                since=trading_constants.NO_DATA_LIMIT,\n                until=trading_constants.NO_DATA_LIMIT\n            )\n            assert cancel_order_mock.mock_calls[0].args == (\"order_1\", )\n            assert cancel_order_mock.mock_calls[1].args == (\"order_2\", )\n            cancel_open_orders_mock.assert_not_called()\n            cancel_order_mock.reset_mock()\n\n        # no order to cancel\n        with mock.patch.object(order_tags, \"get_tagged_orders\", mock.Mock(return_value=[])) as get_tagged_orders_mock:\n            assert await cancelling.cancel_orders(mock_context, which=\"tag1\") is False\n            get_tagged_orders_mock.assert_called_once_with(\n                mock_context, \"tag1\", symbol=None,\n                since=trading_constants.NO_DATA_LIMIT,\n                until=trading_constants.NO_DATA_LIMIT\n            )\n            cancel_order_mock.assert_not_called()\n            cancel_open_orders_mock.assert_not_called()\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/static/config.json",
    "content": "{\n  \"time_frame\": [\"1h\", \"4h\", \"1d\"],\n  \"exchanges\": {\n    \"binance\": {\n      \"api-key\": \"\",\n      \"api-secret\": \"\",\n      \"web-socket\": false\n    },\n    \"bitmex\": {\n      \"api-key\": \"\",\n      \"api-secret\": \"\",\n      \"web-socket\": false\n    },\n    \"poloniex\": {\n      \"api-key\": \"\",\n      \"api-secret\": \"\",\n      \"web-socket\": false\n    }\n  }\n}"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/static/profile.json",
    "content": "{\n  \"profile\": {\n    \"avatar\": \"default_profile.png\",\n    \"description\": \"OctoBot default profile.\",\n    \"id\": \"default\",\n    \"name\": \"default\"\n  },\n  \"config\": {\n    \"crypto-currencies\": {\n      \"Bitcoin\": {\n        \"pairs\": [\n          \"BTC/USDT\",\n          \"BTC/EUR\",\n          \"BTC/USDC\"\n        ]\n      },\n      \"Neo\": {\n        \"pairs\": [\n          \"NEO/BTC\"\n        ]\n      },\n      \"Ethereum\": {\n        \"pairs\": [\n          \"ETH/USDT\"\n        ]\n      },\n      \"Icon\": {\n        \"pairs\": [\n          \"ICX/BTC\"\n        ]\n      },\n      \"VeChain\": {\n        \"pairs\": [\n          \"VEN/BTC\"\n        ]\n      },\n      \"Nano\": {\n        \"pairs\": [\n          \"XRB/BTC\"\n        ]\n      },\n      \"Cardano\": {\n        \"pairs\": [\n          \"ADA/BTC\"\n        ]\n      },\n      \"Ontology\": {\n        \"pairs\": [\n          \"ONT/BTC\"\n        ]\n      },\n      \"Stellar\": {\n        \"pairs\": [\n          \"XLM/BTC\"\n        ]\n      },\n      \"Power Ledger\": {\n        \"pairs\": [\n          \"POWR/BTC\"\n        ]\n      },\n      \"Ethereum Classic\": {\n        \"pairs\": [\n          \"ETC/BTC\"\n        ]\n      },\n      \"WAX\": {\n        \"pairs\": [\n          \"WAX/BTC\"\n        ]\n      },\n      \"XRP\": {\n        \"pairs\": [\n          \"XRP/BTC\"\n        ]\n      },\n      \"Verge\": {\n        \"pairs\": [\n          \"XVG/BTC\"\n        ]\n      }\n    },\n    \"exchanges\": {},\n    \"trading\": {\n      \"risk\": 1,\n      \"reference-market\": \"BTC\"\n    },\n    \"trader\": {\n      \"enabled\": false\n    },\n    \"trader-simulator\": {\n      \"enabled\": true,\n      \"starting-portfolio\": {\n        \"BTC\": 10,\n        \"USDT\": 1000\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/test_utils/__init__.py",
    "content": ""
  },
  {
    "path": "Meta/Keywords/scripting_library/tests/test_utils/order_util.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom octobot_commons.asyncio_tools import wait_asyncio_next_cycle\n\n\nasync def fill_limit_or_stop_order(limit_or_stop_order):\n    await limit_or_stop_order.on_fill()\n    await wait_asyncio_next_cycle()\n\n\nasync def fill_market_order(market_order):\n    await market_order.on_fill()\n    await wait_asyncio_next_cycle()\n"
  },
  {
    "path": "README.md",
    "content": "# OctoBot-Tentacles\n[![OctoBot-Tentacles-CI](https://github.com/Drakkar-Software/OctoBot-Tentacles/workflows/OctoBot-Tentacles-CI/badge.svg)](https://github.com/Drakkar-Software/OctoBot-Tentacles/actions)\n\nThis repository contains default evaluators, strategies, utilitary modules, interfaces and trading modes for the [OctoBot](https://github.com/Drakkar-Software/OctoBot) project.\n\nModules in this tentacles are installed in the **Default** folder of the associated module types\n\nTo add custom tentacles to your OctoBot, see the [dedicated docs page](https://www.octobot.cloud/guides/octobot-tentacles-development/customize-your-octobot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_tentacles_readme).\n\n## Contributing to the official OctoBot Tentacles:\n1. Create your own fork of this repo\n2. Start your branch from the `dev` branch of this repo\n3. Commit and push your changes into your fork\n4. Create a pull request from your branch on your fork to the `dev` branch of this repo\n\nTips:\n\nTo export changes from your local OctoBot tentacles folder into this repo, run this command from your OctoBot folder:   \n`python start.py tentacles -e \"../../OctoBot-Tentacles\" OctoBot-Default-Tentacles -d \"tentacles\"`  \nWhere: \n- `start.py`: start.py script from your OctoBot folder\n- `tentacles`: the tentacles command of the script\n- `../../OctoBot-Tentacles`: the path to your fork of this repository (relatively to the folder you are running the command from)\n- `OctoBot-Default-Tentacles`: filter to only export tentacles tagged as `OctoBot-Default-Tentacles` (in metadata file)\n- `-d tentacles`: name of your OctoBot tentacles folder that are to be copied to the repo (relatively to the folder you are running the command from)\n"
  },
  {
    "path": "Services/Interfaces/telegram_bot_interface/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .telegram_bot import TelegramBotInterface"
  },
  {
    "path": "Services/Interfaces/telegram_bot_interface/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramBotInterface\"],\n  \"tentacles-requirements\": [\"telegram_service\"]\n}"
  },
  {
    "path": "Services/Interfaces/telegram_bot_interface/telegram_bot.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport logging\nimport time\nimport threading\nimport telegram.ext\nimport telegram.constants\nimport telegram.error\n\nimport octobot_commons.constants as commons_constants\nimport octobot_services.constants as services_constants\nimport octobot_services.interfaces.bots as interfaces_bots\nimport tentacles.Services.Services_bases as Services_bases\n\n\n# Telegram bot interface\n# telegram markdown reminder: *bold*, _italic_, `code`, [text_link](http://github.com/)\n\n\nclass TelegramBotInterface(interfaces_bots.AbstractBotInterface):\n    REQUIRED_SERVICES = [Services_bases.TelegramService]\n    HANDLED_CHATS = [telegram.constants.ChatType.PRIVATE]\n    LAST_ERROR_TIMESTAMPS = {}\n    ERROR_LEVEL_INTERVALS_THRESHOLD = 1 * commons_constants.MINUTE_TO_SECONDS\n\n    def __init__(self, config):\n        super().__init__(config)\n        self.telegram_service: Services_bases.TelegramService = None\n\n    async def _post_initialize(self, _):\n        self.telegram_service = Services_bases.TelegramService.instance()\n        self.telegram_service.register_user(self.get_name())\n        self.telegram_service.add_handlers(self.get_bot_handlers())\n        self.telegram_service.add_error_handler(self.command_error)\n        self.telegram_service.register_text_polling_handler(self.HANDLED_CHATS, self.echo)\n        return True\n\n    async def _inner_start(self) -> bool:\n        if self.telegram_service:\n            await self.telegram_service.start_bot(TelegramBotInterface.handle_polling_error)\n            return True\n        else:\n            # debug level log only: error log is already produced in initialize()\n            self.get_logger().debug(\n                f\"Impossible to start bot interface: {self.REQUIRED_SERVICES[0].get_name()} is unavailable.\"\n            )\n            return False\n\n    async def stop(self):\n        await self.telegram_service.stop()\n\n    def get_bot_handlers(self):\n        return [\n            telegram.ext.CommandHandler(\"start\", self.command_start),\n            telegram.ext.CommandHandler(\"ping\", self.command_ping),\n            telegram.ext.CommandHandler([\"portfolio\", \"pf\"], self.command_portfolio),\n            telegram.ext.CommandHandler([\"open_orders\", \"oo\"], self.command_open_orders),\n            telegram.ext.CommandHandler([\"trades_history\", \"th\"], self.command_trades_history),\n            telegram.ext.CommandHandler([\"profitability\", \"pb\"], self.command_profitability),\n            telegram.ext.CommandHandler([\"fees\", \"fs\"], self.command_fees),\n            telegram.ext.CommandHandler(\"sell_all\", self.command_sell_all),\n            telegram.ext.CommandHandler(\"sell_all_currencies\", self.command_sell_all_currencies),\n            telegram.ext.CommandHandler(\"set_risk\", self.command_risk),\n            telegram.ext.CommandHandler([\"market_status\", \"ms\"], self.command_market_status),\n            telegram.ext.CommandHandler([\"configuration\", \"cf\"], self.command_configuration),\n            telegram.ext.CommandHandler([\"refresh_portfolio\", \"rpf\"], self.command_portfolio_refresh),\n            telegram.ext.CommandHandler([\"version\", \"v\"], self.command_version),\n            telegram.ext.CommandHandler(\"stop\", self.command_stop),\n            telegram.ext.CommandHandler(\"restart\", self.command_restart),\n            telegram.ext.CommandHandler([\"help\", \"h\"], self.command_help),\n            telegram.ext.CommandHandler([\"pause\", \"resume\"], self.command_pause_resume),\n            telegram.ext.MessageHandler(telegram.ext.filters.COMMAND, self.command_unknown)\n        ]\n\n    @staticmethod\n    async def command_unknown(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update,\n                f\"`Unfortunately, I don't know the command:`\"\n                f\"{telegram.helpers.escape_markdown(update.effective_message.text)}.\"\n            )\n\n    @staticmethod\n    async def command_help(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            message = \"* - My OctoBot skills - *\" + interfaces_bots.EOL + interfaces_bots.EOL\n            message += \"/start: `Displays my startup message.`\" + interfaces_bots.EOL\n            message += \"/ping: `Shows for how long I'm working.`\" + interfaces_bots.EOL\n            message += \"/portfolio or /pf: `Displays my current portfolio.`\" + interfaces_bots.EOL\n            message += \"/open\\_orders or /oo: `Displays my current open orders.`\" + interfaces_bots.EOL\n            message += \"/trades\\_history or /th: `Displays my trades history since I started.`\" + interfaces_bots.EOL\n            message += \"/profitability or /pb: `Displays the profitability I made since I started.`\" + interfaces_bots.EOL\n            message += \"/market\\_status or /ms: `Displays my understanding of the market and my risk parameter.`\" + interfaces_bots.EOL\n            message += \"/fees or /fs: `Displays the total amount of fees I paid since I started.`\" + interfaces_bots.EOL\n            message += \"/configuration or /cf: `Displays my traders, exchanges, evaluators, strategies and trading \" \\\n                       \"mode.`\" + interfaces_bots.EOL\n            message += \"* - Trading Orders - *\" + interfaces_bots.EOL\n            message += \"/sell\\_all : `Cancels all my orders related to the currency in parameter and instantly \" \\\n                       \"liquidate my holdings in this currency for my reference market.`\" + interfaces_bots.EOL\n            message += \"/sell\\_all\\_currencies : `Cancels all my orders and instantly liquidate all my currencies \" \\\n                       \"for my reference market.`\" + interfaces_bots.EOL\n            message += \"* - Management - *\" + interfaces_bots.EOL\n            message += \"/set\\_risk: `Changes my current risk setting into your command's parameter.`\" + interfaces_bots.EOL\n            message += \"/refresh\\_portfolio or /rpf : `Forces OctoBot's real trader portfolio refresh using exchange \" \\\n                       \"data. Should normally not be necessary.`\" + interfaces_bots.EOL\n            message += \"/pause or /resume: `Pauses or resumes me.`\" + interfaces_bots.EOL\n            message += \"/restart: `Restarts me.`\" + interfaces_bots.EOL\n            message += \"/stop: `Stops me.`\" + interfaces_bots.EOL\n            message += \"/version or /v: `Displays my current software version.`\" + interfaces_bots.EOL\n            message += \"/help: `Displays this help.`\"\n            await update.effective_message.reply_markdown(message)\n        elif TelegramBotInterface._is_authorized_chat(update):\n            await update.effective_message.reply_text(interfaces_bots.UNAUTHORIZED_USER_MESSAGE)\n\n    @staticmethod\n    async def command_start(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, interfaces_bots.AbstractBotInterface.get_command_start(markdown=True)\n            )\n        elif TelegramBotInterface._is_authorized_chat(update):\n            await TelegramBotInterface._send_message(update, interfaces_bots.UNAUTHORIZED_USER_MESSAGE)\n\n    @staticmethod\n    async def command_restart(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(update, \"I'll come back !\")\n            threading.Thread(\n                target=interfaces_bots.AbstractBotInterface.set_command_restart,\n                name=\"Restart bot from telegram command\"\n            ).start()\n\n    @staticmethod\n    async def command_stop(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(update, \"_I'm leaving this world..._\")\n            # start interfaces_bots.AbstractBotInterface.set_command_stop in a new thread to finish the current\n            # python-telegram-bot update loop (python-telegram-bot updater can't stop within a loop, therefore\n            # to be able to stop the telegram interface, this command has to return before the telegram bot can\n            # can be stopped, otherwise telegram#stop ends up deadlocking)\n            threading.Thread(\n                target=interfaces_bots.AbstractBotInterface.set_command_stop,\n                name=\"Stop bot from telegram command\"\n            ).start()\n\n    @staticmethod\n    async def command_version(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, f\"`{interfaces_bots.AbstractBotInterface.get_command_version()}`\"\n            )\n\n    async def command_pause_resume(self, update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            if self.paused:\n                await TelegramBotInterface._send_message(\n                    update,\n                    f\"_Resuming..._{interfaces_bots.EOL}`I will restart trading when I see opportunities !`\"\n                )\n                self.set_command_resume()\n            else:\n                await TelegramBotInterface._send_message(\n                    update, f\"_Pausing..._{interfaces_bots.EOL}`I'm cancelling my orders.`\"\n                )\n                await self.set_command_pause()\n\n    @staticmethod\n    async def command_ping(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, f\"`{interfaces_bots.AbstractBotInterface.get_command_ping()}`\"\n            )\n\n    @staticmethod\n    async def command_risk(update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            try:\n                result_risk = interfaces_bots.AbstractBotInterface.set_command_risk(decimal.Decimal(context.args[0]))\n                await TelegramBotInterface._send_message(update, f\"`Risk successfully set to {result_risk}.`\")\n            except Exception:\n                await TelegramBotInterface._send_message(\n                    update, \"`Failed to set new risk, please provide a number between 0 and 1.`\"\n                )\n\n    @staticmethod\n    async def command_profitability(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, interfaces_bots.AbstractBotInterface.get_command_profitability(markdown=True)\n            )\n\n    @staticmethod\n    async def command_fees(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, interfaces_bots.AbstractBotInterface.get_command_fees(markdown=True)\n            )\n\n    @staticmethod\n    async def command_sell_all_currencies(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, f\"`{await interfaces_bots.AbstractBotInterface.get_command_sell_all_currencies()}`\"\n            )\n\n    @staticmethod\n    async def command_sell_all(update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            currency = context.args[0]\n            if not currency:\n                await TelegramBotInterface._send_message(update, \"`Require a currency in parameter of this command.`\")\n            else:\n                await TelegramBotInterface._send_message(\n                    update, f\"`{await interfaces_bots.AbstractBotInterface.get_command_sell_all(currency)}`\"\n                )\n\n    @staticmethod\n    async def command_portfolio(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(update, interfaces_bots.AbstractBotInterface.get_command_portfolio(\n                markdown=True))\n\n    @staticmethod\n    async def command_open_orders(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, interfaces_bots.AbstractBotInterface.get_command_open_orders(markdown=True)\n            )\n\n    @staticmethod\n    async def command_trades_history(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, interfaces_bots.AbstractBotInterface.get_command_trades_history(markdown=True)\n            )\n\n    # refresh current order lists and portfolios and reload tham from exchanges\n    @staticmethod\n    async def command_portfolio_refresh(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            result = \"Refresh\"\n            try:\n                await interfaces_bots.AbstractBotInterface.set_command_portfolios_refresh()\n                await TelegramBotInterface._send_message(update, f\"`{result} successful`\")\n            except Exception as e:\n                await TelegramBotInterface._send_message(update, f\"`{result} failure: {e}`\")\n\n    # Displays my trades, exchanges, evaluators, strategies and trading\n    @staticmethod\n    async def command_configuration(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            try:\n                await TelegramBotInterface._send_message(\n                    update,\n                    interfaces_bots.AbstractBotInterface.get_command_configuration(markdown=True)\n                )\n            except Exception:\n                await TelegramBotInterface._send_message(\n                    update,\n                    \"`I'm unfortunately currently unable to show you my configuration. \"\n                    \"Please wait for my initialization to complete.`\"\n                )\n\n    @staticmethod\n    async def command_market_status(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            try:\n                await TelegramBotInterface._send_message(\n                    update, interfaces_bots.AbstractBotInterface.get_command_market_status(markdown=True)\n                )\n            except Exception:\n                await TelegramBotInterface._send_message(\n                    update,\n                    \"`I'm unfortunately currently unable to show you my market evaluations, \"\n                    \"please retry in a few seconds.`\"\n                )\n\n    @staticmethod\n    async def command_error(update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if update is None:\n            TelegramBotInterface.get_logger().error(\n                f\"Command error with no telegram update. This should not happen. \"\n                f\"context.error: {context} context: {context}\"\n            )\n            return\n        TelegramBotInterface.get_logger().warning(\"Command receiver error. Please check logs for more details.\") \\\n            if context.error is None else TelegramBotInterface.get_logger().exception(context.error, False)\n        if update is not None and TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(\n                update, f\"Failed to perform this command {update.effective_message.text} : `{context.error}`\"\n            )\n\n    @staticmethod\n    def handle_polling_error(error):\n        if isinstance(error, (telegram.error.NetworkError, telegram.error.Conflict)):\n            if isinstance(error, telegram.error.Conflict):\n                error_message = f\"The configured Telegram bot is already connected to a different \" \\\n                                f\"software. Please create a different Telegram bot for each of your simultaneous \" \\\n                                f\"OctoBots ({error})\"\n            else:\n                error_message = f\"Telegram bot error: {error} ({error.__class__.__name__})\"\n            if TelegramBotInterface.get_error_log_level(error) is logging.ERROR:\n                TelegramBotInterface.get_logger().error(error_message)\n            elif TelegramBotInterface.get_error_log_level(error) is logging.WARNING:\n                TelegramBotInterface.get_logger().warning(error_message)\n            else:\n                TelegramBotInterface.get_logger().debug(error_message)\n        else:\n            TelegramBotInterface.get_logger().error(\n                f\"Unexpected telegram bot error: {error} ({error.__class__.__name__})\"\n            )\n\n    @staticmethod\n    def get_error_log_level(error):\n        try:\n            if time.time() - TelegramBotInterface.LAST_ERROR_TIMESTAMPS[error.__class__] > \\\n               TelegramBotInterface.ERROR_LEVEL_INTERVALS_THRESHOLD:\n                TelegramBotInterface.LAST_ERROR_TIMESTAMPS[error.__class__] = time.time()\n                return logging.ERROR\n            return logging.DEBUG\n        except KeyError:\n            TelegramBotInterface.LAST_ERROR_TIMESTAMPS[error.__class__] = time.time()\n            return logging.ERROR\n\n    @staticmethod\n    async def echo(update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        if TelegramBotInterface._is_valid_user(update):\n            await TelegramBotInterface._send_message(update, update.effective_message.text, markdown=False)\n\n    @staticmethod\n    def enable(config, is_enabled, associated_config=services_constants.CONFIG_TELEGRAM):\n        interfaces_bots.AbstractBotInterface.enable(config, is_enabled, associated_config=associated_config)\n\n    @staticmethod\n    def is_enabled(config, associated_config=services_constants.CONFIG_TELEGRAM):\n        return interfaces_bots.AbstractBotInterface.is_enabled(config, associated_config=associated_config)\n\n    @staticmethod\n    def _is_authorized_chat(update: telegram.Update):\n        return update.effective_chat.type in TelegramBotInterface.HANDLED_CHATS\n\n    @staticmethod\n    def _is_valid_user(update: telegram.Update, associated_config=services_constants.CONFIG_TELEGRAM):\n        # only authorize users from a private chat\n        if not TelegramBotInterface._is_authorized_chat(update):\n            return False\n        is_valid, white_list = interfaces_bots.AbstractBotInterface._is_valid_user(\n            update.effective_chat.username, associated_config=associated_config\n        )\n        if white_list and not is_valid:\n            TelegramBotInterface.get_logger().error(\n                f\"An unauthorized Telegram user is trying to talk to me: username: {update.effective_chat.username}, \"\n                f\"first_name: {update.effective_chat.first_name}, text: {update.effective_message.text}\"\n            )\n        return is_valid\n\n    @staticmethod\n    async def _send_message(update: telegram.Update, message: str, markdown=True):\n        messages = interfaces_bots.AbstractBotInterface._split_messages_if_too_long(\n            message,\n            telegram.constants.MessageLimit.MAX_TEXT_LENGTH,\n            interfaces_bots.EOL\n        )\n        for m in messages:\n            if markdown:\n                await update.effective_message.reply_markdown(m)\n            else:\n                await update.effective_message.reply_text(m)\n"
  },
  {
    "path": "Services/Interfaces/telegram_bot_interface/tests/test_bot_interface.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport asyncio\nimport contextlib\n\nimport octobot_services.interfaces as interfaces\nimport octobot.octobot as octobot\nimport octobot.constants as octobot_constants\nimport octobot.producers as octobot_producers\nimport octobot.producers as trading_producers\nimport octobot.community as community\nimport octobot_commons.tests.test_config as test_config\nimport octobot_tentacles_manager.loaders as loaders\nimport octobot_evaluators.api as evaluator_api\nimport tests.test_utils.config as test_utils_config\n\n# All test coroutines will be treated as marked.\n\npytestmark = pytest.mark.asyncio\n\n\nasync def create_minimalist_unconnected_octobot():\n    # import here to prevent later web interface import issues\n    community.IdentifiersProvider.use_production()\n    octobot_instance = octobot.OctoBot(test_config.load_test_config(dict_only=False))\n    octobot_instance.initialized = True\n    tentacles_config = test_utils_config.load_test_tentacles_config()\n    loaders.reload_tentacle_by_tentacle_class()\n    octobot_instance.task_manager.async_loop = asyncio.get_event_loop()\n    octobot_instance.task_manager.create_pool_executor()\n    octobot_instance.tentacles_setup_config = tentacles_config\n    octobot_instance.configuration_manager.add_element(octobot_constants.TENTACLES_SETUP_CONFIG_KEY, tentacles_config)\n    octobot_instance.exchange_producer = trading_producers.ExchangeProducer(None, octobot_instance, None, False)\n    octobot_instance.evaluator_producer = octobot_producers.EvaluatorProducer(None, octobot_instance)\n    await evaluator_api.initialize_evaluators(octobot_instance.config, tentacles_config)\n    octobot_instance.evaluator_producer.matrix_id = evaluator_api.create_matrix()\n    return octobot_instance\n\n\n# use context manager instead of fixture to prevent pytest threads issues\n@contextlib.asynccontextmanager\nasync def get_bot_interface():\n    bot_interface = interfaces.AbstractBotInterface({})\n    interfaces.AbstractInterface.initialize_global_project_data(\n        (await create_minimalist_unconnected_octobot()).octobot_api,\n        \"octobot\",\n        \"x.y.z-alpha42\")\n    yield bot_interface\n\n\nasync def test_all_commands():\n    \"\"\"\n    Test basing commands interactions, for most of them a default message will be saying that the bot is not ready.\n    :return: None\n    \"\"\"\n    async with get_bot_interface() as bot_interface:\n        assert len(bot_interface.get_command_configuration()) > 50\n        assert len(bot_interface.get_command_market_status()) > 50\n        assert len(bot_interface.get_command_trades_history()) > 50\n        assert len(bot_interface.get_command_open_orders()) > 50\n        assert len(bot_interface.get_command_fees()) > 50\n        assert \"Decimal\" not in bot_interface.get_command_fees()\n        assert \"Nothing to sell\" in await bot_interface.get_command_sell_all_currencies()\n        assert \"Nothing to sell for BTC\" in await bot_interface.get_command_sell_all(\"BTC\")\n        with pytest.raises(RuntimeError):\n            await bot_interface.set_command_portfolios_refresh()\n        assert len(bot_interface.get_command_portfolio()) > 50\n        assert \"Decimal\" not in bot_interface.get_command_portfolio()\n        assert len(bot_interface.get_command_profitability()) > 50\n        assert \"Decimal\" not in bot_interface.get_command_profitability()\n        assert \"I'm alive since\" in bot_interface.get_command_ping()\n        assert all(elem in bot_interface.get_command_version()\n                   for elem in\n                   [interfaces.AbstractInterface.project_name, interfaces.AbstractInterface.project_version])\n        assert \"Hello, I'm OctoBot\" in bot_interface.get_command_start()\n        assert await bot_interface.set_command_pause() is None\n"
  },
  {
    "path": "Services/Interfaces/web_interface/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport collections\nimport logging\nimport abc\nimport os.path\n\nimport octobot_commons.logging as bot_logging\nimport octobot_commons.timestamp_util as timestamp_util\nimport octobot_services.enums as services_enums\n\n\nclass Notifier:\n    @abc.abstractmethod\n    def send_notifications(self) -> bool:\n        raise NotImplementedError(\"send_notifications is not implemented\")\n\n\nnotifiers = {}\n\n\ndef register_notifier(notification_key, notifier):\n    if notification_key not in notifiers:\n        notifiers[notification_key] = []\n    notifiers[notification_key].append(notifier)\n\n\nGENERAL_NOTIFICATION_KEY = \"general_notifications\"\nBACKTESTING_NOTIFICATION_KEY = \"backtesting_notifications\"\nDATA_COLLECTOR_NOTIFICATION_KEY = \"data_collector_notifications\"\nSTRATEGY_OPTIMIZER_NOTIFICATION_KEY = \"strategy_optimizer_notifications\"\nDASHBOARD_NOTIFICATION_KEY = \"dashboard_notifications\"\n\nimport octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    # Make WebInterface visible to imports\n    from tentacles.Services.Interfaces.web_interface.web import WebInterface\n\n\n# disable server logging\nfor logger in ('engineio.server', 'socketio.server', 'geventwebsocket.handler'):\n    logging.getLogger(logger).setLevel(logging.WARNING)\n\nMAX_NOTIFICATION_HISTORY_SIZE = 1000\nMAX_NOTIFICATION_AT_ONCE = 10\nnotifications_history = collections.deque(maxlen=MAX_NOTIFICATION_HISTORY_SIZE)\nnotifications = collections.deque(maxlen=MAX_NOTIFICATION_AT_ONCE)\n# Different from notifications_history: this list \"should\" never be cleared by more recent notifications.\n# maxsize is here just in case to avoid memory leaks\ncritical_notifications = collections.deque(maxlen=MAX_NOTIFICATION_HISTORY_SIZE)\n\nTIME_AXIS_TITLE = \"Time\"\n\n\ndef dir_last_updated(folder):\n    update_times = [\n        os.path.getmtime(os.path.join(root_path, f))\n        for root_path, dirs, files in os.walk(folder)\n        for f in files\n    ]\n    return str(max(update_times + [0]))  # add 0 not to crash if no files are found\n\n\nLAST_UPDATED_STATIC_FILES = 0\n\n\ndef update_registered_plugins(plugins):\n    global LAST_UPDATED_STATIC_FILES\n    last_update_time = max(\n        float(LAST_UPDATED_STATIC_FILES),\n        float(dir_last_updated(os.path.join(os.path.dirname(__file__), \"static\")))\n    )\n    for plugin in plugins:\n        if plugin.static_folder:\n            last_update_time = max(\n                last_update_time,\n                float(dir_last_updated(plugin.static_folder))\n            )\n    LAST_UPDATED_STATIC_FILES = last_update_time\n\n\ndef flush_notifications():\n    notifications.clear()\n\n\ndef _send_notification(notification_key, **kwargs) -> bool:\n    if notification_key in notifiers:\n        return any(notifier.all_clients_send_notifications(**kwargs)\n                   for notifier in notifiers[notification_key])\n    return False\n\n\ndef send_general_notifications(**kwargs):\n    if _send_notification(GENERAL_NOTIFICATION_KEY, **kwargs):\n        flush_notifications()\n\n\ndef send_backtesting_status(**kwargs):\n    _send_notification(BACKTESTING_NOTIFICATION_KEY, **kwargs)\n\n\ndef send_data_collector_status(**kwargs):\n    _send_notification(DATA_COLLECTOR_NOTIFICATION_KEY, **kwargs)\n\n\ndef send_strategy_optimizer_status(**kwargs):\n    _send_notification(STRATEGY_OPTIMIZER_NOTIFICATION_KEY, **kwargs)\n\n\ndef send_new_trade(dict_new_trade, exchange_id, symbol):\n    _send_notification(DASHBOARD_NOTIFICATION_KEY, exchange_id=exchange_id, trades=[dict_new_trade], symbol=symbol)\n\n\ndef send_order_update(dict_order, exchange_id, symbol):\n    _send_notification(DASHBOARD_NOTIFICATION_KEY, exchange_id=exchange_id, order=dict_order, symbol=symbol)\n\n\nasync def add_notification(level: services_enums.NotificationLevel, title, message, sound=None):\n    notification = {\n        \"Level\": level.value,\n        \"Title\": title,\n        \"Message\": message.replace(\"<br>\", \" \"),\n        \"Sound\": sound,\n        \"Time\": timestamp_util.get_now_time()\n    }\n    notifications.append(notification)\n    notifications_history.append(notification)\n    if level == services_enums.NotificationLevel.CRITICAL:\n        critical_notifications.append(notification)\n    send_general_notifications()\n\n\ndef get_notifications() -> list:\n    return list(notifications)\n\n\ndef get_notifications_history() -> list:\n    return list(notifications_history)\n\n\ndef get_critical_notifications() -> list:\n    return list(critical_notifications)\n\n\ndef get_logs():\n    return bot_logging.logs_database[bot_logging.LOG_DATABASE]\n\n\ndef get_errors_count():\n    return bot_logging.logs_database[bot_logging.LOG_NEW_ERRORS_COUNT]\n\n\ndef flush_errors_count():\n    bot_logging.reset_errors_count()\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_controllers/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport octobot.enums\n\nimport tentacles.Services.Interfaces.web_interface.advanced_controllers.configuration\nimport tentacles.Services.Interfaces.web_interface.advanced_controllers.home\nimport tentacles.Services.Interfaces.web_interface.advanced_controllers.matrix\nimport tentacles.Services.Interfaces.web_interface.advanced_controllers.strategy_optimizer\nimport tentacles.Services.Interfaces.web_interface.advanced_controllers.tentacles_management\n\n\ndef register(distribution: octobot.enums.OctoBotDistribution):\n    blueprint = flask.Blueprint('advanced', __name__, url_prefix='/advanced', template_folder=\"../advanced_templates\")\n    if distribution is octobot.enums.OctoBotDistribution.DEFAULT:\n        tentacles.Services.Interfaces.web_interface.advanced_controllers.configuration.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.advanced_controllers.home.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.advanced_controllers.matrix.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.advanced_controllers.strategy_optimizer.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.advanced_controllers.tentacles_management.register(blueprint)\n\n    return blueprint\n\n\n__all__ = [\n    \"register\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_controllers/configuration.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/evaluator_config\")\n    @blueprint.route('/evaluator_config', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def evaluator_config():\n        if flask.request.method == 'POST':\n            request_data = flask.request.get_json()\n            success = True\n            response = \"\"\n\n            if request_data:\n                # update evaluator config if required\n                if constants.EVALUATOR_CONFIG_KEY in request_data and request_data[constants.EVALUATOR_CONFIG_KEY]:\n                    success = success and models.update_tentacles_activation_config(\n                        request_data[constants.EVALUATOR_CONFIG_KEY])\n\n                response = {\n                    \"evaluator_updated_config\": request_data[constants.EVALUATOR_CONFIG_KEY]\n                }\n\n            if success:\n                if request_data.get(\"restart_after_save\", False):\n                    models.schedule_delayed_command(models.restart_bot)\n                return util.get_rest_reply(flask.jsonify(response))\n            else:\n                return util.get_rest_reply('{\"update\": \"ko\"}', 500)\n        else:\n            media_url = flask.url_for(\"tentacle_media\", _external=True)\n            missing_tentacles = set()\n            return flask.render_template(\n                'advanced_evaluator_config.html',\n                evaluator_config=models.get_evaluator_detailed_config(media_url, missing_tentacles),\n                evaluator_startup_config=models.get_evaluators_tentacles_startup_activation(),\n                missing_tentacles=missing_tentacles,\n                current_profile=models.get_current_profile()\n            )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_controllers/home.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/\")\n    @blueprint.route(\"/home\")\n    @login.login_required_when_activated\n    def home():\n        return flask.render_template('advanced_index.html')\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_controllers/matrix.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_services.interfaces.util as util\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/matrix\")\n    @login.login_required_when_activated\n    def matrix():\n        return flask.render_template('advanced_matrix.html',\n                                     matrix_list=util.get_matrix_list())\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_controllers/strategy_optimizer.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    # strategy optimize is disabled\n    return\n\n    @blueprint.route(\"/strategy-optimizer\")\n    @blueprint.route('/strategy-optimizer', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def strategy_optimizer():\n        if not models.is_backtesting_enabled():\n            return flask.redirect(flask.url_for(\"home\"))\n        if flask.request.method == 'POST':\n            update_type = flask.request.args[\"update_type\"]\n            request_data = flask.request.get_json()\n            success = False\n            reply = \"Operation OK\"\n\n            if update_type == \"cancel_optimizer\":\n                try:\n                    success, reply = models.cancel_optimizer()\n                except Exception as e:\n                    return util.get_rest_reply('{\"start_optimizer\": \"ko: ' + str(e) + '\"}', 500)\n            elif request_data:\n                if update_type == \"start_optimizer\":\n                    try:\n                        strategy = request_data[\"strategy\"][0]\n                        time_frames = request_data[\"time_frames\"]\n                        evaluators = request_data[\"evaluators\"]\n                        risks = request_data[\"risks\"]\n                        success, reply = models.start_optimizer(strategy, time_frames, evaluators, risks)\n                    except Exception as e:\n                        return util.get_rest_reply('{\"start_optimizer\": \"ko: ' + str(e) + '\"}', 500)\n\n            if success:\n                return util.get_rest_reply(flask.jsonify(reply))\n            else:\n                return util.get_rest_reply(reply, 500)\n\n        elif flask.request.method == 'GET':\n            if flask.request.args:\n                target = flask.request.args[\"update_type\"]\n                if target == \"optimizer_results\":\n                    optimizer_results = models.get_optimizer_results()\n                    return flask.jsonify(optimizer_results)\n                if target == \"optimizer_report\":\n                    optimizer_report = models.get_optimizer_report()\n                    return flask.jsonify(optimizer_report)\n                if target == \"strategy_params\":\n                    strategy_name = flask.request.args[\"strategy_name\"]\n                    params = {\n                        \"time_frames\": list(models.get_time_frames_list(strategy_name)),\n                        \"evaluators\": list(models.get_evaluators_list(strategy_name))\n                    }\n                    return flask.jsonify(params)\n            else:\n                trading_mode = models.get_config_activated_trading_mode()\n                strategies = models.get_strategies_list(trading_mode)\n                current_strategy = strategies[0] if strategies else \"\"\n                return flask.render_template('advanced_strategy_optimizer.html',\n                                             strategies=strategies,\n                                             current_strategy=current_strategy,\n                                             time_frames=models.get_time_frames_list(current_strategy),\n                                             evaluators=models.get_evaluators_list(current_strategy),\n                                             risks=models.get_risks_list(),\n                                             trading_mode=trading_mode.get_name() if trading_mode else None,\n                                             run_params=models.get_current_run_params())\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_controllers/tentacles_management.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport octobot_commons.authentication as authentication\nimport octobot.constants as constants\n\n\ndef register(blueprint):\n    @blueprint.route(\"/tentacles\")\n    @login.active_login_required\n    def tentacles():\n        return flask.render_template(\"advanced_tentacles.html\",\n                                     tentacles=models.get_tentacles())\n    \n    \n    def _handle_package_operation(update_type):\n        if update_type == \"add_package\":\n            request_data = flask.request.get_json()\n            success = False\n            if request_data:\n                version = None\n                url_key = \"url\"\n                if url_key in request_data:\n                    path_or_url = request_data[url_key]\n                    version = request_data.get(\"version\", None)\n                    action = \"register_and_install\"\n                else:\n                    path_or_url, action = next(iter(request_data.items()))\n                    path_or_url = path_or_url.strip()\n                if action == \"register_and_install\":\n                    installation_result = models.install_packages(\n                        path_or_url,\n                        version,\n                        authenticator=authentication.Authenticator.instance())\n                    if installation_result:\n                        return util.get_rest_reply(flask.jsonify(installation_result))\n                    else:\n                        return util.get_rest_reply('Impossible to install the given tentacles package. '\n                                                   'Please see logs for more details.', 500)\n    \n            if not success:\n                return util.get_rest_reply('{\"operation\": \"ko\"}', 500)\n        elif update_type in [\"install_packages\", \"update_packages\", \"reset_packages\"]:\n    \n            packages_operation_result = {}\n            if update_type == \"install_packages\":\n                packages_operation_result = models.install_packages()\n            elif update_type == \"update_packages\":\n                packages_operation_result = models.update_packages()\n            elif update_type == \"reset_packages\":\n                packages_operation_result = models.reset_packages()\n    \n            if packages_operation_result:\n                return util.get_rest_reply(flask.jsonify(packages_operation_result))\n            else:\n                action = update_type.split(\"_\")[0]\n                return util.get_rest_reply(f'Impossible to {action} packages, check the logs for more information.', 500)\n    \n    \n    def _handle_module_operation(update_type):\n        request_data = flask.request.get_json()\n        if request_data:\n            packages_operation_result = {}\n            if update_type == \"update_modules\":\n                packages_operation_result = models.update_modules(request_data)\n            elif update_type == \"uninstall_modules\":\n                packages_operation_result = models.uninstall_modules(request_data)\n    \n            if packages_operation_result is not None:\n                return util.get_rest_reply(flask.jsonify(packages_operation_result))\n            else:\n                action = update_type.split(\"_\")[0]\n                return util.get_rest_reply(f'Impossible to {action} module(s), check the logs for more information.', 500)\n        else:\n            return util.get_rest_reply('{\"Need at least one element be selected\": \"ko\"}', 500)\n    \n    \n    def _handle_tentacles_pages_post(update_type):\n        if update_type in [\"add_package\", \"install_packages\", \"update_packages\", \"reset_packages\"]:\n            return _handle_package_operation(update_type)\n    \n        elif update_type in [\"update_modules\", \"uninstall_modules\"]:\n            return _handle_module_operation(update_type)\n    \n    \n    @blueprint.route('/install_official_tentacle_packages<use_beta_tentacles>', methods=['POST'])\n    @login.login_required_when_activated\n    def install_official_tentacle_packages(use_beta_tentacles):\n        bundle_url = models.get_official_tentacles_url(use_beta_tentacles == \"True\")\n        packages_operation_result = models.install_packages(path_or_url=bundle_url)\n        if packages_operation_result:\n            return util.get_rest_reply(flask.jsonify(packages_operation_result))\n        else:\n            return util.get_rest_reply(f'Impossible to install tentacles, check the logs for more information.', 500)\n    \n    \n    @blueprint.route(\"/tentacle_packages\")\n    @blueprint.route('/tentacle_packages', methods=['GET', 'POST'])\n    @login.active_login_required\n    def tentacle_packages():\n        if flask.request.method == 'POST':\n            if not constants.CAN_INSTALL_TENTACLES:\n                return util.get_rest_reply(f'Impossible to install tentacles on this cloud OctoBot.', 500)\n            update_type = flask.request.args[\"update_type\"]\n            return _handle_tentacles_pages_post(update_type)\n    \n        else:\n            return flask.render_template(\"advanced_tentacle_packages.html\",\n                                         get_tentacles_packages=models.get_tentacles_packages)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_evaluator_config.html",
    "content": "{% extends \"advanced_layout.html\" %}\n{% set active_page = \"advanced.evaluator_config\" %}\n{% import 'components/config/evaluator_card.html' as m_config_evaluator_card %}\n{% import 'macros/tentacles.html' as m_tentacles %}\n\n{% block additional_style %}\n    <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/components/configuration.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n{% endblock additional_style %}\n\n{% block body %}\n<div id=\"nav-config\">\n    <div class=\"card card-header\">\n        <h3>\n            <span class=\"float-left\">\n                <a href=\"{{ url_for('profile') }}\">\n                    <i class=\"fas fa-arrow-left\"></i>\n                </a>\n            </span>\n            &ensp;Activated strategies: {{ ', '.join(evaluator_config[\"activated_strategies\"]) }}\n        </h3>\n    </div>\n    {% if not current_profile.read_only %}\n        <div class=\"navbar nav-tabs navbar-dark primary-color\">\n            <span class=\"white-text\"> Any configuration change will be applied after OctoBot restarts.</span>\n            <ul class=\"nav mx-auto\">\n                <li class=\"nav-item\">\n                    <a class=\"btn btn-primary waves-effect\" id='save-config' href=\"#\" role=\"tab\" update-url=\"{{ url_for('advanced.evaluator_config') }}\">Save</a>\n                </li>\n                <li class=\"nav-item\">\n                    <a class=\"btn btn-outline-primary waves-effect\" id='reset-config' href=\"#\" role=\"tab\">Reset all</a>\n                </li>\n            </ul>\n            <ul class=\"nav ml-auto\">\n                <li class=\"nav-item\">\n                    <button id='save-config-and-restart' update-url=\"{{ url_for('advanced.evaluator_config') }}\" type=\"button\" class=\"btn btn-outline-primary waves-effect\">Apply changes and restart</button>\n                </li>\n            </ul>\n        </div>\n    {% endif %}\n</div>\n<div id=\"super-container\">\n    {% if not current_profile.read_only %}\n        <div class=\"config-root\" id=\"panelEvaluators\"><br>\n            {{ m_tentacles.missing_tentacles_warning(missing_tentacles) }}\n            <div class=\"container-fluid alert alert-info mx-0 mt-2\" role=\"alert\">\n                <p>\n                    <i class=\"fa-regular fa-lightbulb\"></i>\n                    Select evaluators and configure to use in your profile.\n                    Please note with for some strategies, there might be <i class=\"fa fa-flag\"></i> required evaluators.\n                </p>\n            </div>\n            <div class=\"card\">\n                <div class=\"card-header\"><h2>Technical analysis</h2></div>\n                <div class=\"card-body\">\n                    <div class=\"row config-container\" id=\"ta-evaluator-config-root\">\n                        {% for evaluator_name, info in evaluator_config[\"ta\"].items() %}\n                            {{ m_config_evaluator_card.config_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                        {% endfor %}\n                    </div>\n                </div>\n            </div>\n            <br>\n            <div class=\"card\">\n                <div class=\"card-header\"><h2>Social analysis</h2></div>\n                <div class=\"card-body\">\n                    <div class=\"row config-container\" id=\"social-evaluator-config-root\">\n                        {% for evaluator_name, info in evaluator_config[\"social\"].items() %}\n                            {{ m_config_evaluator_card.config_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                        {% endfor %}\n                    </div>\n                </div>\n            </div>\n            <br>\n            <div class=\"card\">\n                <div class=\"card-header\"><h2>Scripted evaluators</h2></div>\n                <div class=\"card-body\">\n                    <div class=\"row config-container\" id=\"scripted-evaluator-config-root\">\n                        {% for evaluator_name, info in evaluator_config[\"scripted\"].items() %}\n                            {{ m_config_evaluator_card.config_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                        {% endfor %}\n                    </div>\n                </div>\n            </div>\n            <br>\n            <div class=\"card\">\n                <div class=\"card-header\"><h2 style=\"display:inline;\">Real time analysis</h2> <h4 style=\"display:inline;\">\n                    <i class=\"fas fa-exclamation-triangle\"></i> Should only be used with exchanges supporting\n                    <a  target=\"_blank\" rel=\"noopener\" href=\"{{EXCHANGES_DOCS_URL}}\" >websocket connection</a></h4>\n                </div>\n                <div class=\"card-body\">\n                    <div class=\"row config-container\" id=\"rt-evaluator-config-root\">\n                        {% for evaluator_name, info in evaluator_config[\"real-time\"].items() %}\n                            {{ m_config_evaluator_card.config_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                        {% endfor %}\n                    </div>\n                </div>\n                <div class=\"card-footer\">\n                    <p class=\"mb-0\">Back to <a class=\"btn btn-outline-primary btn-md waves-effect\" href=\"{{ url_for('profile') }}\">OctoBot standard configuration</a></p>\n                </div>\n            </div>\n        </div>\n    {% else %}\n        <div class=\"card\">\n            <div class=\"card-body\">\n                Current profile is read only. To be able to change the currently enabled evaluators,\n                please duplicate your current profile by using \"duplicate\" button\n                in <a href={{ url_for('profile') }}>profile page</a> using the \"Edit profiles\" menu.\n            </div>\n        </div>\n    {% endif %}\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/evaluator_configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_index.html",
    "content": "{% extends \"advanced_layout.html\" %}\n{% set active_page = \"advanced.home\" %}\n{% block body %}\n<br>\n<div class=\"card text-center w-50 mx-auto\">\n    <h2 class=\"card-header\">Welcome to OctoBot's advanced interface</h2>\n    {% with messages = get_flashed_messages(with_categories=true) %}\n      {% if messages %}\n            {% for category, message in messages %}\n            <div class=\"alert alert-{{ 'danger' if category == 'error' else 'success' }}\">\n                {{ message }}\n            </div>\n            {% endfor %}\n      {% endif %}\n    {% endwith %}\n    <div class=\"card-body py-4\">\n        This interface is providing insights on some OctoBot advanced concepts and should be used once OctoBot basic\n        features are understood.\n    </div>\n    <div class=\"card-footer\">\n        <a class=\"btn btn-sm btn-outline-primary waves-effect\" href=\"{{ url_for('home') }}\"><i class=\"fa fa-home\"></i> Back to OctoBot</a>\n    </div>\n</div>\n\n{% endblock %}"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_layout.html",
    "content": "{% import 'components/community/user_details.html' as m_user_details %}\n\n<!doctype html>\n<html lang=\"en\" data-mdb-theme=\"{{get_color_mode()}}\">\n    {% set active_page = active_page|default('advanced.home') -%}\n    <head>\n        <title>{{ active_page.split(\".\")[-1] | replace(\"_\", \" \") | capitalize }} - OctoBot</title>\n\n        <!-- Required meta tags -->\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n        <!-- Prevent search engines indexing -->\n        <meta name=\"robots\" content=\"noindex\">\n\n        <!-- Favicon -->\n        <link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.png') }}\">\n\n        {% block additional_meta %}\n        {% endblock additional_meta %}\n\n        <!-- Bootstrap CSS -->\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css\" integrity=\"sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2\" crossorigin=\"anonymous\">\n\n        <!-- Fontawesome CSS -->\n        <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v6.4.0/css/all.css\" integrity=\"sha384-iw3OoTErCYJJB9mCa8LNS2hbsQ7M3C0EpIsO/H5+EGAkPGc6rk+V8i04oW/K5xq0\" crossorigin=\"anonymous\">\n        <!-- toaster -->\n        <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\" integrity=\"sha384-YzEqZ2pBV0i9OmlTyoz75PqwTR8If8GsXBv7HLQclEVqIC3VxIt98/U94ES6CJTR\" crossorigin=\"anonymous\">\n\n        <!-- mdb CSS -->\n        <link href=\"https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/7.3.2/mdb.min.css\" integrity=\"kj1RBJ7aqGUnavWQDbYyovF5HQGHlvNf6SZ2CfaCNkoBJBEux2JXFCXqGZTAYENh\" rel=\"stylesheet\" crossorigin=\"anonymous\">\n\n        <!-- Datatables -->\n        <link rel=\"stylesheet\" href=\"https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.min.css\" integrity=\"sha384-zUxWDVAcow8yNu+q4VFsyZA3qWsKKGdWPW0SVjaR12LQze4SY8Nr75US6VDhbWkf\" crossorigin=\"anonymous\">\n\n        <!-- Select -->\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/css/bootstrap-select.min.css\" integrity=\"sha384-2SvkxRa9G/GlZMyFexHk+WN9p0n2T+r38dvBmw5l2/J3gjUcxs9R1GwKs0seeSh3\" crossorigin=\"anonymous\">\n\n        <!-- Editable -->\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/bootstrap-editable.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css\" integrity=\"sha384-OXVF05DQEe311p6ohU11NwlnX08FzMCsyoXzGOaL+83dKAb3qS17yZJxESl8YrJQ\" crossorigin=\"anonymous\">\n\n        <!-- Own -->\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/style.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/layout.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n\n        {% block additional_style %}\n        {% endblock additional_style %}\n    </head>\n    <body style=\"{% block body_style %}{% endblock body_style %}\">\n        <!-- Scripts -->\n        <!-- At the beginning of the page : be available for template scripts -->\n        <script src=\"https://code.jquery.com/jquery-3.6.4.min.js\" integrity=\"sha384-UG8ao2jwOWB7/oDdObZc6ItJmwUkR/PfMyt9Qs5AwX7PsnYn1CRKCTWyncPTWvaS\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.6.1/socket.io.min.js\" integrity=\"sha384-KA7m0DwgQGmeRC6Xre3hJO+ZxpanOauVh4Czdqbg8lDKJ3bZZYVYmP+y4F31x40L\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js\" integrity=\"sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\" integrity=\"sha384-VDls8ImYGI8SwVxpmjX2Bn27U2TcNodzTNROTusVEWO55+lmL+H9NczoQJk6mwZR\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/7.3.2/mdb.umd.min.js\" integrity=\"sha384-TGRlbFTmiVIUuSy+b/aj9mHaUTABC3gid02pJimnu14vfLMvOzODXgRmw03nf7vs\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js\" integrity=\"sha384-GP2+CwBlakZSDJUr+E4JvbxpM75i1i8+RKkieQxzuyDZLG+5105E1OfHIjzcXyWH\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.plot.ly/plotly-2.20.0.min.js\" integrity=\"sha384-lqNbLAc8irUVsiXijo8d5LY0Ecc43bEe85kyAJgdi+CAvmBPO/L1SWp6EUxChKM/\" crossorigin=\"anonymous\"></script>\n\n        <script src=\"https://cdn.datatables.net/2.0.8/js/dataTables.min.js\" integrity=\"sha384-nJy9D0UBD2LV93ED7IXSsdWfa9PumZvn70zRSR/oFw5Zq0x6gWwWdpLeGsbVATVg\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/js/bootstrap-select.min.js\" integrity=\"sha384-SfMwgGnc3UiUUZF50PsPetXLqH2HSl/FmkMW/Ja3N2WaJ/fHLbCHPUsXzzrM6aet\"  crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js\" integrity=\"sha384-d3UHjPdzJkZuk5H3qKYMLRyWLAQBJbby2yr2Q58hXXtAGF8RSNO9jpLDlKKPv5v3\" crossorigin=\"anonymous\"></script>\n\n        <script src=\"{{ url_for('static', filename='js/lib/bootstrap-editable.min.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/cst.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/util.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/bot_connection.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/dom_updater.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/required.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        {{ m_user_details.posthog(IS_DEMO, IS_CLOUD, IS_ALLOWING_TRACKING, PH_TRACKING_ID) }}\n\n        <nav class=\"navbar navbar-expand-md py-0 py-md-2\" id=\"main-nav-bar\">\n            <div class=\"navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2\">\n                <ul class=\"navbar-nav mr-auto\">\n                    <li class=\"nav-item mx-1 px-0 my-auto {% if 'advanced.home' == active_page %} active{% endif %}\">\n                        <a class=\"nav-link\" href=\"{{ url_for('advanced.home') }}\">Home</a>\n                    </li>\n                    <li class=\"nav-item mx-1 px-0 my-auto {% if 'advanced.matrix' == active_page %} active{% endif %}\">\n                        <a class=\"nav-link\" href=\"{{ url_for('advanced.matrix') }}\">Evaluation matrix</a>\n                    </li>\n                    <li class=\"nav-item mx-1 px-0 my-auto {% if 'advanced.evaluator_config' == active_page %} active{% endif %}\">\n                        <a class=\"nav-link\" href=\"{{ url_for('advanced.evaluator_config') }}\">Evaluator configuration</a>\n                    </li>\n                </ul>\n            </div>\n            <div class=\"mx-auto order-0\">\n                <a class=\"navbar-brand mx-auto\" href=\"{{ url_for('advanced.home') }}\">Advanced OctoBot\n                    <i id=\"navbar-bot-status\" class=\"fa fa-check\" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"OctoBot operational\"></i>\n                </a>\n                <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".dual-collapse2\">\n                    <span class=\"navbar-toggler-icon\"></span>\n                </button>\n            </div>\n            <div class=\"navbar-collapse collapse w-100 order-3 dual-collapse2\">\n                <ul class=\"navbar-nav ml-auto\">\n                    {% if is_advanced_interface_enabled %}\n                        {% if CAN_INSTALL_TENTACLES %}\n                        <li class=\"nav-item mx-1 px-0 my-auto {% if 'advanced.tentacles' == active_page %} active{% endif %}\">\n                            <a class=\"nav-link\" href=\"{{ url_for('advanced.tentacles') }}\">Tentacles</a>\n                        </li>\n                        {% endif %}\n                        <li class=\"nav-item mx-1 px-0 my-auto {% if 'logs' == active_page %} active{% endif %}\">\n                            <a id=\"theme-switch\" class=\"nav-link\" href=\"#\" aria-label=\"Switch theme\" data-update-url=\"{{url_for('api.display_config')}}\">\n                                <i class=\"{{'fa fa-moon' if get_color_mode() == 'light' else 'fas fa-sun'}}\" data-toggle=\"tooltip\" data-placement=\"top\"\n                                   title=\"Use {{'dark' if get_color_mode() == 'light' else 'light'}} theme\">\n                            </i></a>\n                        </li>\n                        <li class=\"nav-item mx-1 px-0 my-auto\">\n                            <a class=\"nav-link\" href=\"{{ url_for('home') }}\"><i class=\"fa fa-home\"></i> Back to OctoBot</a>\n                        </li>\n                    {% endif %}\n                </ul>\n            </div>\n        </nav>\n\n        <div class=\"container-fluid\">\n            {% block body %}{% endblock %}\n        </div>\n\n        <!-- Artificial padding to separate footer from the rest of the page -->\n        <div class=\"pb-5\"></div>\n        <div class=\"pb-5\"></div>\n        <!-- Artificial padding  -->\n\n        {% include \"distributions/default/footer.html\" %}\n\n        {% block additional_scripts %}\n        {% endblock additional_scripts %}\n        {{ m_user_details.user_details(\n            USER_EMAIL,\n            USER_SELECTED_BOT_ID,\n            has_open_source_package,\n            PROFILE_NAME,\n            TRADING_MODE_NAME,\n            EXCHANGE_NAMES,\n            IS_REAL_TRADING\n        ) }}\n    </body>\n</html>\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_matrix.html",
    "content": "{% extends \"advanced_layout.html\" %}\n{% set active_page = \"advanced.matrix\" %}\n{% block body %}\n<div class=\"card\">\n    <div class=\"card-header\" id=\"matrixViewPage\">\n        <h1>\n            Matrix View\n        </h1>\n    </div>\n    <div class=\"card-body\">\n        <div class=\"container-fluid row mx-0\">\n            <div class=\"col-md-3 mb-3\">\n                Evaluators :\n                <select class=\"mx-0 selectpicker\" id=\"evaluatorsSelect\" data-width=\"50%\" data-window-padding=\"25\" multiple>\n                </select>\n            </div>\n            <div class=\"col-md-3 mb-3\">\n                Time frames :\n                <select class=\"mx-0 selectpicker\" id=\"timeframesSelect\" data-width=\"50%\" data-window-padding=\"25\" multiple>\n                </select>\n            </div>\n            <div class=\"col-md-3 mb-3\">\n                Symbols :\n                <select class=\"mx-0 selectpicker\" id=\"symbolsSelect\" data-width=\"50%\" data-window-padding=\"25\" multiple>\n                </select>\n            </div>\n            <div class=\"col-md-3 mb-3\">\n                Exchanges :\n                <select class=\"mx-0 selectpicker\" id=\"exchangesSelect\" data-width=\"50%\" data-window-padding=\"25\" multiple>\n                </select>\n            </div>\n        </div>\n        <table class=\"table table-striped table-sm table-responsive-sm\" id=\"matrixDataTable\">\n          <thead>\n            <tr>\n                <th scope=\"col\">Evaluator</th>\n                <th scope=\"col\">Value</th>\n                <th scope=\"col\">Time frame</th>\n                <th scope=\"col\">Symbol</th>\n                <th scope=\"col\">Exchange</th>\n            </tr>\n          </thead>\n          <tbody>\n            {% for exchange, matrix_exchange in matrix_list.items() %}\n                {% for evaluator, matrix_evaluator in matrix_exchange.items() %}\n                    {% for symbol, matrix_symbol in matrix_evaluator.items() %}\n                        {% if matrix_symbol is iterable and matrix_symbol is not string %}\n                            {% for time_frame, eval_note in matrix_symbol.items() %}\n                                 <tr>\n                                    <td>{{evaluator}}</td>\n                                    <td>{{eval_note}}</td>\n                                    <td>{{time_frame}}</td>\n                                    <td>{{symbol}}</td>\n                                    <td>{{exchange}}</td>\n                                 </tr>\n                            {% endfor %}\n                        {% else %}\n                            <tr>\n                                <td>{{evaluator}}</td>\n                                <td>{{matrix_symbol}}</td>\n                                <td></td>\n                                <td>{{symbol}}</td>\n                                <td>{{exchange}}</td>\n                            </tr>\n                        {% endif %}\n                    {% endfor %}\n                {% endfor %}\n            {% endfor %}\n          </tbody>\n        </table>\n        <div class=\"container-fluid alert alert-info mx-0 mt-2\" role=\"alert\">\n            <p>\n                <i class=\"fa-regular fa-lightbulb\"></i>\n                The matrix view shows the current value of each active evaluator and strategy.\n            </p>\n            <p>\n                For evaluators and strategies returning a number between -1 and 1 like the RSIMomentumEvaluator or\n                DoubleMovingAverageTrendEvaluator, values are to be understood this way:\n                <ul>\n                    <li>\n                        -1: Strong buy signal\n                    </li>\n                    <li>\n                        Between -1 and 0: Buy signal, the closer to -1 the stronger\n                    </li>\n                    <li>\n                        0: Neutral or undefined\n                    </li>\n                    <li>\n                        Between 0 and 1: Sell signal, the closer to -1 the stronger\n                    </li>\n                    <li>\n                        1: Strong sell signal\n                    </li>\n                </ul>\n            </p>\n        </div>\n    </div>\n</div>\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/advanced_matrix.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_strategy_optimizer.html",
    "content": "{% extends \"advanced_layout.html\" %}\n{% set active_page = \"advanced.strategy_optimizer\" %}\n{% block body %}\n<br>\n<div class=\"card\" id=\"strategyOptimizerInputs\">\n    <div class=\"card-header\">\n        <h1>Strategy optimizer</h1>\n    </div>\n    <div class=\"card-body\" id=\"paramSettings\" update-url=\"{{ url_for('advanced.strategy_optimizer', update_type='run_params') }}\">\n        <div class=\"alert alert-info\" role=\"alert\">\n            <p class=\"mb-0\">\n                For now the strategy optimizer can only be used with the daily trading mode as is helps to\n                identify better evaluators configurations and this trading mode is the only one supporting\n                custom evaluators setups.\n            </p>\n        </div>\n        <div class=\"input-group\">\n          <div class=\"input-group-prepend mb-3\">\n            <label class=\"input-group-text\" for=\"tradingModeSelect\">\n                Trading Mode\n            </label>\n          </div>\n          <select class=\"custom-select mb-9\" id=\"tradingModeSelect\" disabled>\n            <option value={{trading_mode}} selected=\"selected\">\n              {{trading_mode}}\n            </option>\n          </select>\n        </div>\n        <div class=\"input-group\">\n          <div class=\"input-group-prepend mb-3\">\n            <label class=\"input-group-text\" for=\"strategySelect\">\n                Strategy\n            </label>\n          </div>\n          <select class=\"custom-select mb-9\" id=\"strategySelect\" update-url=\"{{ url_for('advanced.strategy_optimizer', update_type='strategy_params') }}\">\n            {% for strategy in strategies %}\n                <option value={{strategy}}\n                    {% if run_params['strategy_name'] and strategy in run_params['strategy_name'] %}\n                        selected=\"selected\"\n                    {% elif strategy == current_strategy and not run_params['strategy_name'] %}\n                        selected=\"selected\"\n                    {% endif %}>\n                      {{strategy}}\n                </option>\n            {% endfor %}\n          </select>\n        </div>\n        <div class=\"input-group\">\n          <div class=\"input-group-prepend mb-3\">\n            <label class=\"input-group-text\" for=\"evaluatorsSelect\">Evaluators</label>\n          </div>\n          <select class=\"custom-select multi-select-element mb-9\" id=\"evaluatorsSelect\" multiple=\"multiple\">\n            {% for evaluator in evaluators %}\n                <option value={{evaluator}}\n                    {% if run_params['evaluators'] and evaluator in run_params['evaluators'] %}\n                        selected=\"selected\"\n                    {% elif loop.index == 1 and not run_params['evaluators'] %}\n                        selected=\"selected\"\n                    {% endif %}>\n                      {{evaluator}}\n                </option>\n            {% endfor %}\n          </select>\n        </div>\n        <div class=\"input-group\">\n          <div class=\"input-group-prepend mb-3\">\n            <label class=\"input-group-text\" for=\"timeFramesSelect\">Time Frames</label>\n          </div>\n          <select class=\"custom-select multi-select-element mb-9\" id=\"timeFramesSelect\" multiple=\"multiple\">\n            {% for timeframe in time_frames %}\n                <option value={{timeframe}}\n                    {% if run_params['time_frames'] and timeframe in run_params['time_frames'] %}\n                        selected=\"selected\"\n                    {% elif loop.index == 1 and not run_params['time_frames']%}\n                        selected=\"selected\"\n                    {% endif %}>\n                      {{timeframe}}\n                </option>\n            {% endfor %}\n          </select>\n        </div>\n        <div class=\"input-group\">\n          <div class=\"input-group-prepend mb-3\">\n            <label class=\"input-group-text\" for=\"risksSelect\">Risks</label>\n          </div>\n          <select class=\"custom-select multi-select-element mb-9\" id=\"risksSelect\" multiple=\"multiple\">\n            {% for risk in risks %}\n                <option value={{risk}}\n                    {% if run_params['risks'] and risk in run_params['risks'] %}\n                        selected=\"selected\"\n                    {% elif loop.index == 1 and not run_params['risks'] %}\n                        selected=\"selected\"\n                    {% endif %}>\n                      {{risk}}\n                </option>\n            {% endfor %}\n          </select>\n        </div>\n        <h2>Number of simulations <span id=\"numberOfSimulatons\" class=\"badge badge-light\">0</span></h2>\n        <span id='progess_bar' style='display: none;'>\n            <div class=\"card-title\">\n                <h2>Strategy optimizer in progress</h2>\n            </div>\n            <div>\n                <canvas id=\"optimize_doughnutChart\" height=\"70%\"></canvas>\n            </div>\n            <div class='progress'>\n                <div id='progess_bar_anim' class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='0' aria-valuemin='0' aria-valuemax='100' style='width: 0%;'></div>\n            </div>\n        </span>\n        <button id=\"startOptimizer\" type=\"button\" class=\"btn btn-primary waves-effect\" update-url=\"{{ url_for('advanced.strategy_optimizer', update_type='start_optimizer') }}\">Start optimizer</button>\n        <div class=\"alert alert-info\" role=\"alert\">\n            <p class=\"mb-0\">\n                <i class=\"fa-regular fa-lightbulb\"></i>\n                If you want to deeply test your strategy, compare its results in different situations and figure out\n                the best settings for your traded markets, we suggest to check out the\n                <a href=\"https://www.octobot.cloud/{{LOCALE}}/guides/octobot-usage/strategy-designer?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=strategy_optimizer\" target=\"_blank\" rel=\"noopener\">\n                    Strategy Designer </a>\n                available on <a href=\"{{OCTOBOT_COMMUNITY_URL}}/trading-bot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=strategy_optimizer\" target=\"_blank\" rel=\"noopener\">\n                <i class=\"fa-brands fa-octopus-deploy\"></i> OctoBot cloud trading bots plans</a>.\n            </p>\n        </div>\n    </div>\n</div>\n<br>\n<div id=\"error_info\" class=\"alert alert-danger\" role=\"alert\" style='display: none;'>\n    <div id=\"error_info_text\"></div>\n    <a class=\"nav-link\" href=\"{{ url_for('logs') }}\">Details</a>\n</div>\n<br>\n<div id=\"results_datatable_card\" class=\"card\" style='display: none;'>\n    <div class=\"card-header\"><h2>Results</h2></div>\n    <div id=\"report_datatable_card\" class=\"card-body\" style='display: none;'>\n        <table id=\"report_datatable\" class=\"table table-striped table-responsive-lg\" width=\"95%\" update-url=\"{{ url_for('advanced.strategy_optimizer', update_type='optimizer_report') }}\">\n          <caption>Optimizer global report</caption>\n          <thead>\n            <tr>\n                <th scope=\"col\">#</th>\n                <th scope=\"col\">Evaluator(s)</th>\n                <th scope=\"col\">Risk</th>\n                <th scope=\"col\">Average trades count</th>\n                <th scope=\"col\">Comparative score: the lower the better</th>\n            </tr>\n          </thead>\n          <tbody>\n          </tbody>\n        </table>\n    </div>\n    <div class=\"card-body\">\n      <table class=\"table table-striped table-responsive-lg\"  id=\"results_datatable\" width=\"95%\" update-url=\"{{ url_for('advanced.strategy_optimizer', update_type='optimizer_results') }}\">\n          <caption>Iterations results</caption>\n          <thead>\n            <tr>\n                <th scope=\"col\">#</th>\n                <th scope=\"col\">Evaluator(s)</th>\n                <th scope=\"col\">Time Frame(s)</th>\n                <th scope=\"col\">Risk</th>\n                <th scope=\"col\">Average trades count</th>\n                <th scope=\"col\">Score: the higher the better</th>\n            </tr>\n          </thead>\n          <tbody>\n          </tbody>\n      </table>\n    </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/strategy_optimizer.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script>\n    recompute_nb_iterations();\n</script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_tentacle_packages.html",
    "content": "{% extends \"advanced_layout.html\" %}\n{% set active_page = \"advanced.tentacles\" %}\n<script src=\"{{ url_for('static', filename='js/components/tentacles.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>\n            <span class=\"float-left\">\n                <a href=\"{{ url_for('advanced.tentacles') }}\">\n                    <i class=\"fas fa-arrow-left\"></i>\n                </a>\n            </span>\n            &ensp;Tentacle Packages\n            <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-advanced-usage/tentacle-manager#add-new-tentacles-packages-to-your-octobot\">\n                <i class=\"fa fa-question\"></i>\n            </a>\n        </h2>\n    </div>\n</div>\n<br>\n<div class=\"card\">\n    <div class=\"card-header\"><h2>\n        Registered tentacles packages\n    </h2></div>\n    <div class=\"card-body\">\n        <table class=\"table table-striped table-sm table-responsive-lg\" id=\"tentacles_packages_table\">\n          <thead>\n            <tr>\n                <th scope=\"col\">Package Name</th>\n                <th scope=\"col\">Package origin location</th>\n            </tr>\n          </thead>\n          <tbody>\n            {% for package_name, package_path in get_tentacles_packages().items() %}\n            <tr>\n                <td>{{package_name}}</td>\n                <td>{{package_path}}</td>\n            </tr>\n            {% endfor %}\n          </tbody>\n        </table>\n        <br>\n        <h2>Additional tentacles packages registration</h2>\n        <div class=\"config-root\" update-url=\"{{ url_for('advanced.tentacle_packages', update_type='add_package') }}\">\n          <div class='progress' id='register_and_install_package_progess_bar' style='display: none;'>\n              <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='100' aria-valuemin='0' aria-valuemax='100' style='width: 100%;'></div>\n          </div>\n          <div class=\"input-group mb-3\">\n              <input type=\"text\" class=\"form-control\" placeholder=\"Package localisation or url\" aria-label=\"Package localisation or url\" id=\"register_and_install_package_input\">\n              <div class=\"input-group-append\">\n                <button class=\"btn btn-md btn-outline-success config-root m-0 px-3 py-2 z-depth-0 waves-effect\" type=\"submit\" onclick=\"register_and_install_package()\" id=\"register_and_install_package_button\">Register and install</button>\n              </div>\n          </div>\n        </div>\n    </div>\n</div>\n<br>\n<div class=\"card\">\n    <div class=\"card-header\"><h2>Packages management</h2></div>\n    <div class=\"card-body\">\n        <div class='progress' id='packages_action_progess_bar' style='display: none;'>\n            <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='100' aria-valuemin='0' aria-valuemax='100' style='width: 100%;'></div>\n        </div>\n        <div class=\"btn-group btn-group mx-auto\" role=\"group\">\n            <button type=\"button\" class=\"btn btn-success card-link waves-effect\" id=\"install_tentacles_packages\" update-url=\"{{ url_for('advanced.tentacle_packages', update_type='install_packages') }}\">Re-install registered tentacles packages</button>\n            <button type=\"button\" class=\"btn btn-primary card-link waves-effect\" id=\"update_tentacles_packages\" update-url=\"{{ url_for('advanced.tentacle_packages', update_type='update_packages') }}\">Update installed packages</button>\n            <button type=\"button\" class=\"btn btn-danger card-link waves-effect\" id=\"reset_tentacles_packages\" update-url=\"{{ url_for('advanced.tentacle_packages', update_type='reset_packages') }}\">Remove installed packages</button>\n        </div>\n  </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/tentacles_configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/advanced_templates/advanced_tentacles.html",
    "content": "{% extends \"advanced_layout.html\" %}\n{% set active_page = \"advanced.tentacles\" %}\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>Installed Tentacles\n        <span class=\"float-right\">\n            <a class=\"blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-advanced-usage/tentacle-manager?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=advanced_tentacles\">\n                <i class=\"fa fa-question\"></i>\n            </a>\n        </span>\n        </h2>\n    </div>\n    <div class=\"card-body\">\n        <span id=\"table-span\">\n            <table class=\"table table-striped table-sm table-responsive-lg\" id=\"tentacles_modules_table\">\n              <thead>\n                <tr>\n                    <th scope=\"col\">#</th>\n                    <th scope=\"col\">Package</th>\n                    <th scope=\"col\">Name</th>\n                    <th scope=\"col\">Type</th>\n                    <th scope=\"col\">Version</th>\n                    <th scope=\"col\" class=\"text-center\">Action</th>\n                </tr>\n              </thead>\n              <tbody id=\"module-table\" update-update-url=\"{{ url_for('advanced.tentacle_packages', update_type='update_modules') }}\" uninstall-update-url=\"{{ url_for('advanced.tentacle_packages', update_type='uninstall_modules') }}\">\n                {% for tentacle in tentacles %}\n                    <tr>\n                        <td class=\"selectable_tentacle\"><div class=\"custom-control custom-checkbox\">\n                            <input type=\"checkbox\" class=\"custom-control-input tentacle-module-checkbox\" module=\"{{tentacle.name}}\">\n                            <label class=\"custom-control-label\"></label>\n                        </div></td>\n                        <td class=\"selectable_tentacle\">{{tentacle.origin_package}}</td>\n                        <td class=\"selectable_tentacle\">{{tentacle.name}}</td>\n                        <td class=\"selectable_tentacle\">{{tentacle.tentacle_type}}</td>\n                        <td class=\"selectable_tentacle\">{{tentacle.version}}</td>\n                        <td class=\"text-center\">\n                            <a class=\"btn btn-sm text-primary waves-effect\" onclick=\"update('{{tentacle.name}}')\" data-toggle=\"tooltip\" data-placement=\"right\" title=\"Update tentacle\"><i class=\"fas fa-download\"></i></a>\n                            <a class=\"btn btn-sm text-primary waves-effect\" onclick=\"uninstall('{{tentacle.name}}')\" data-toggle=\"tooltip\" data-placement=\"right\" title=\"Uninstall tentacle\"><i class=\"fas fa-trash-alt\"></i></a></td>\n                    </tr>\n                {% endfor %}\n              </tbody>\n            </table>\n        </span>\n        <div class='progress' id='selected_tentacles_operation' style='display: none;'>\n          <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='100' aria-valuemin='0' aria-valuemax='100' style='width: 100%;'></div>\n        </div>\n        <div class=\"btn-group btn-group text-center\" role=\"group\">\n            <button type=\"button\" class=\"btn btn-primary card-link waves-effect\" id=\"update_selected_tentacles\" update-url=\"{{ url_for('advanced.tentacle_packages', update_type='update_modules') }}\">Update selected tentacles</button>\n            <button type=\"button\" class=\"btn btn-danger card-link waves-effect\" id=\"uninstall_selected_tentacles\" update-url=\"{{ url_for('advanced.tentacle_packages', update_type='uninstall_modules') }}\">Uninstall selected tentacles</button>\n        </div>\n    </div>\n</div>\n\n<div class=\"card-footer text-center\">\n    <a href=\"{{ url_for('advanced.tentacle_packages') }}\" class=\"btn btn-outline-info btn-lg waves-effect\"><i class=\"fa fa-cloud-download-alt\"></i> Install Tentacles packages</a>\n</div>\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/tentacles_configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport octobot.enums\n\nimport tentacles.Services.Interfaces.web_interface.api.config\nimport tentacles.Services.Interfaces.web_interface.api.exchanges\nimport tentacles.Services.Interfaces.web_interface.api.feedback\nimport tentacles.Services.Interfaces.web_interface.api.metadata\nimport tentacles.Services.Interfaces.web_interface.api.trading\nimport tentacles.Services.Interfaces.web_interface.api.user_commands\nimport tentacles.Services.Interfaces.web_interface.api.bots\nimport tentacles.Services.Interfaces.web_interface.api.webhook\nimport tentacles.Services.Interfaces.web_interface.api.tentacles_packages\nimport tentacles.Services.Interfaces.web_interface.api.dsl\n\nfrom tentacles.Services.Interfaces.web_interface.api.webhook import (\n    has_webhook,\n    register_webhook\n)\n\n\n\ndef register(distribution: octobot.enums.OctoBotDistribution):\n    blueprint = flask.Blueprint('api', __name__, url_prefix='/api', template_folder=\"\")\n    if distribution is octobot.enums.OctoBotDistribution.DEFAULT:\n        tentacles.Services.Interfaces.web_interface.api.feedback.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.api.bots.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.api.webhook.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.api.tentacles_packages.register(blueprint)\n\n    elif distribution is octobot.enums.OctoBotDistribution.MARKET_MAKING:\n        pass\n\n    # common routes\n    tentacles.Services.Interfaces.web_interface.api.config.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.api.exchanges.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.api.metadata.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.api.trading.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.api.user_commands.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.api.dsl.register(blueprint)\n    return blueprint\n\n\n__all__ = [\n    \"has_webhook\",\n    \"register_webhook\",\n    \"register\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/bots.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot.community as community\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/select_bot\", methods=['POST'])\n    @login.login_required_when_activated\n    def select_bot():\n        if not models.can_select_bot():\n            return util.get_rest_reply(flask.jsonify(\"Can't select bot on this setup\"), 500)\n        models.select_bot(flask.request.get_json())\n        bot = models.get_selected_user_bot()\n        flask.flash(f\"Selected {bot['name']} bot\", \"success\")\n        return flask.jsonify(bot)\n\n\n    @blueprint.route(\"/create_bot\", methods=['POST'])\n    @login.login_required_when_activated\n    def create_bot():\n        if not models.can_select_bot():\n            return util.get_rest_reply(flask.jsonify(\"Can't create bot on this setup\"), 500)\n        new_bot = models.create_new_bot()\n        models.select_bot(community.CommunityUserAccount.get_bot_id(new_bot))\n        bot = models.get_selected_user_bot()\n        flask.flash(f\"Created and selected {bot['name']} bot\", \"success\")\n        return flask.jsonify(bot)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/config.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.authentication\nimport octobot.community.errors\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route('/get_config_currency', methods=[\"GET\"])\n    @login.login_required_when_activated\n    def get_config_currency():\n        return flask.jsonify(models.format_config_symbols(interfaces_util.get_edited_config()))\n\n\n    @blueprint.route('/get_all_currencies/<exchange>', methods=[\"GET\"])\n    @login.login_required_when_activated\n    def get_all_currencies(exchange):\n        return flask.jsonify(models.get_all_currencies([exchange]))\n\n\n    @blueprint.route('/get_all_symbols/<exchange>')\n    @login.login_required_when_activated\n    def get_all_symbols(exchange):\n        return flask.jsonify(models.get_symbol_list([exchange]))\n\n\n    @blueprint.route('/set_config_currency', methods=[\"POST\"])\n    @login.login_required_when_activated\n    def set_config_currency():\n        request_data = flask.request.get_json()\n        success, reply = models.update_config_currencies(\n            request_data[\"currencies\"],\n            replace=(request_data.get(\"action\", \"update\") == \"replace\")\n        )\n        return util.get_rest_reply(flask.jsonify(reply)) if success else util.get_rest_reply(reply, 500)\n\n\n    @blueprint.route('/change_reference_market_on_config_currencies', methods=[\"POST\"])\n    @login.login_required_when_activated\n    def change_reference_market_on_config_currencies():\n        request_data = flask.request.get_json()\n        success, reply = models.change_reference_market_on_config_currencies(request_data[\"old_base_currency\"],\n                                                                             request_data[\"new_base_currency\"])\n        return util.get_rest_reply(flask.jsonify(reply)) if success else util.get_rest_reply(reply, 500)\n\n\n    @blueprint.route('/display_config', methods=[\"POST\"])\n    @login.login_required_when_activated\n    def display_config():\n        request_data = flask.request.get_json()\n        success = False\n        message = \"nothing to do\"\n        if \"color_mode\" in request_data:\n            success, message = models.set_color_mode(request_data[\"color_mode\"])\n        if \"time_frame\" in request_data:\n            success, message = models.set_display_timeframe(request_data[\"time_frame\"])\n        if \"display_orders\" in request_data:\n            success, message = models.set_display_orders(request_data[\"display_orders\"])\n        return util.get_rest_reply(flask.jsonify(message), 200 if success else 500)\n\n\n    @blueprint.route('/hide_announcement<key>', methods=[\"POST\"])\n    @login.login_required_when_activated\n    def hide_announcement(key):\n        models.set_display_announcement(key, False)\n        return util.get_rest_reply(flask.jsonify(\"\"), 200)\n\n\n    @blueprint.route('/start_copy_trading', methods=[\"POST\"])\n    @login.login_required_when_activated\n    def start_copy_trading():\n        try:\n            copy_id = flask.request.get_json()[\"copy_id\"]\n            profile_id = flask.request.get_json()[\"profile_id\"]\n            if models.get_current_profile().profile_id != profile_id:\n                models.select_profile(profile_id)\n            response = f\"{models.get_current_profile().name} profile selected\"\n            success, config_resp = models.update_copied_trading_id(copy_id)\n            response = f\"{response}, {config_resp}\"\n            return util.get_rest_reply(flask.jsonify(response)) if success else util.get_rest_reply(response, 500)\n        except Exception as e:\n            return util.get_rest_reply(f\"Unexpected error : {e}\", 500)\n\n\n    @blueprint.route('/trading_strategies_tentacles_details<backtestable_only>', methods=[\"GET\"])\n    @login.login_required_when_activated\n    def trading_strategies_tentacles_details(backtestable_only):\n        missing_tentacles = set()\n        media_url = flask.url_for(\"tentacle_media\", _external=True)\n        evaluators = {}\n        for evals in models.get_evaluator_detailed_config(media_url, missing_tentacles).values():\n            if isinstance(evals, dict):\n                evaluators.update(evals)\n        strategy_config = models.get_strategy_config(\n            media_url, missing_tentacles, with_trading_modes=True, whitelist=None, backtestable_only=backtestable_only\n        )\n        return flask.jsonify({\n            \"trading_modes\": strategy_config[models.TRADING_MODES_KEY],\n            \"strategies\": strategy_config[models.STRATEGIES_KEY],\n            \"evaluators\": evaluators,\n        })\n\n\n    @blueprint.route('/tradingview_confirm_email_content', methods=[\"GET\"])\n    @login.login_required_when_activated\n    def tradingview_confirm_email_content():\n        try:\n            return util.get_rest_reply(\n                flask.jsonify(models.get_last_email_address_confirm_code_email_content()), 200\n            )\n        except octobot_commons.authentication.AuthenticationRequired:\n            return util.get_rest_reply(flask.jsonify(\"authentication required\"), 401)\n\n\n    @blueprint.route('/trigger_wait_for_email_address_confirm_code_email', methods=[\"POST\"])\n    @login.login_required_when_activated\n    def trigger_wait_for_email_address_confirm_code_email():\n        try:\n            models.wait_for_email_address_confirm_code_email()\n            return util.get_rest_reply(flask.jsonify(\"\"), 200)\n        except octobot.community.errors.ExtensionRequiredError as err:\n            return util.get_rest_reply(flask.jsonify(str(err)), 401)\n\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/dsl.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\"/dsl_keywords_docs\", methods=['GET'])\n    @login.login_required_when_activated\n    def dsl_keywords_docs():\n        return flask.jsonify(\n            [docs.to_json() for docs in models.get_dsl_keywords_docs()]\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/exchanges.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/are_compatible_accounts\", methods=['POST'])\n    @login.login_required_when_activated\n    def are_compatible_accounts():\n        request_data = flask.request.get_json()\n        return flask.jsonify(models.are_compatible_accounts(request_data))\n\n\n    @blueprint.route(\"/first_exchange_details\")\n    @login.login_required_when_activated\n    def first_exchange_details():\n        exchange_name = flask.request.args.get('exchange_name', None)\n        try:\n            exchange_manager, exchange_name, exchange_id = models.get_first_exchange_data(exchange_name)\n            return util.get_rest_reply(\n                {\n                    \"exchange_name\": exchange_name,\n                    \"exchange_id\": exchange_id\n                },\n                200\n            )\n        except KeyError as e:\n            return util.get_rest_reply(str(e), 404)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/feedback.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/register_submitted_form\", methods=['POST'])\n    @login.login_required_when_activated\n    def register_submitted_form():\n        request_data = flask.request.get_json()\n        form_id = request_data[\"form_id\"]\n        user_id = request_data[\"user_id\"]\n        success, message = models.register_user_submitted_form(user_id, form_id)\n        return util.get_rest_reply(flask.jsonify(message), 200 if success else 500)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/metadata.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport json\nimport flask_cors\nimport cachetools\n\nimport octobot.api as octobot_api\nimport octobot.constants as constants\nimport octobot_services.interfaces as interfaces\nimport octobot_commons.constants\nimport octobot_commons.timestamp_util as timestamp_util\n\n\ndef register(blueprint):\n    _LATEST_VERSION_CACHE = cachetools.TTLCache(\n        maxsize=1, ttl=octobot_commons.constants.DAYS_TO_SECONDS\n    )\n\n    @blueprint.route(\"/ping\")\n    @flask_cors.cross_origin()\n    def ping():\n        start_time = interfaces.get_bot_api().get_start_time()\n        return json.dumps(\n            f\"Running since \"\n            f\"{timestamp_util.convert_timestamp_to_datetime(start_time, '%Y-%m-%d %H:%M:%S', local_timezone=True)}.\"\n        )\n\n\n    @blueprint.route(\"/version\")\n    def version():\n        return json.dumps(f\"{interfaces.AbstractInterface.project_name} {interfaces.AbstractInterface.project_version}\")\n\n\n    @blueprint.route(\"/upgrade_version\")\n    def upgrade_version():\n        async def fetch_upgrade_version():\n            updater = octobot_api.get_updater()\n            return await updater.get_latest_version() if updater and await updater.should_be_updated() else None\n\n        # avoid fetching upgrade version if already fetched in the last day\n        try:\n            version = _LATEST_VERSION_CACHE[\"version\"]\n        except KeyError:\n            version = interfaces.run_in_bot_main_loop(fetch_upgrade_version(), timeout=5)\n            _LATEST_VERSION_CACHE[\"version\"] = version\n\n        return json.dumps(version)\n\n\n    @blueprint.route(\"/user_feedback\")\n    def user_feedback():\n        return json.dumps(constants.OCTOBOT_FEEDBACK_FORM_URL)\n\n\n    @blueprint.route(\"/announcements\")\n    def announcements():\n        return \"\"\n        # return json.dumps(\"external_resources_manager.get_external_resource(\n        #     service_constants.EXTERNAL_RESOURCE_PUBLIC_ANNOUNCEMENTS,\n        #     catch_exception=True)\")\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/tentacles_packages.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/checkout_url\", methods=['POST'])\n    @login.login_required_when_activated\n    def checkout_url():\n        request_data = flask.request.get_json()\n        payment_method = request_data[\"paymentMethod\"]\n        redirect_url = request_data[\"redirectUrl\"]\n        success, url = models.get_checkout_url(payment_method, redirect_url)\n        return util.get_rest_reply(\n            {\n                \"url\": url,\n            },\n            200 if success else 500\n        )\n\n    @blueprint.route(\"/has_open_source_package\", methods=['POST'])\n    @login.login_required_when_activated\n    def has_open_source_package():\n        models.update_owned_packages()\n        return util.get_rest_reply(\n            {\n                \"has_open_source_package\": models.has_open_source_package(),\n            },\n            200\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/trading.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\"/orders\", methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def orders():\n        if flask.request.method == 'GET':\n            return flask.jsonify(models.get_all_orders_data())\n        elif flask.request.method == \"POST\":\n            result = \"\"\n            request_data = flask.request.get_json()\n            action = flask.request.args.get(\"action\")\n            if action == \"cancel_order\":\n                if interfaces_util.cancel_orders([request_data]):\n                    result = \"Order cancelled\"\n                else:\n                    return util.get_rest_reply('Impossible to cancel order: order not found.', 500)\n            elif action == \"cancel_orders\":\n                removed_count = interfaces_util.cancel_orders(request_data)\n                result = f\"{removed_count} orders cancelled\"\n            return flask.jsonify(result)\n\n\n    @blueprint.route(\"/trades\", methods=['GET'])\n    @login.login_required_when_activated\n    def trades():\n        return flask.jsonify(models.get_all_trades_data())\n\n\n    @blueprint.route(\"/positions\", methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def positions():\n        if flask.request.method == 'GET':\n            return flask.jsonify(models.get_all_positions_data())\n        elif flask.request.method == \"POST\":\n            result = \"\"\n            request_data = flask.request.get_json()\n            action = flask.request.args.get(\"action\")\n            if action == \"close_position\":\n                if interfaces_util.close_positions([request_data]):\n                    result = \"Position closed\"\n                else:\n                    return util.get_rest_reply('Impossible to close position: position already closed.', 500)\n            return flask.jsonify(result)\n\n\n    @blueprint.route(\"/refresh_portfolio\", methods=['POST'])\n    @login.login_required_when_activated\n    def refresh_portfolio():\n        try:\n            interfaces_util.trigger_portfolios_refresh()\n            return flask.jsonify(\"Portfolio(s) refreshed\")\n        except RuntimeError:\n            return util.get_rest_reply(\"No portfolio to refresh\", 500)\n\n\n    @blueprint.route(\"/currency_list\", methods=['GET'])\n    @login.login_required_when_activated\n    def currency_list():\n        return flask.jsonify(models.get_all_symbols_list())\n\n\n    @blueprint.route(\"/historical_portfolio_value\", methods=['GET'])\n    @login.login_required_when_activated\n    def historical_portfolio_value():\n        currency = flask.request.args.get(\"currency\", \"USDT\")\n        time_frame = flask.request.args.get(\"time_frame\")\n        from_timestamp = flask.request.args.get(\"from_timestamp\")\n        to_timestamp = flask.request.args.get(\"to_timestamp\")\n        exchange = flask.request.args.get(\"exchange\")\n        try:\n            return flask.jsonify(models.get_portfolio_historical_values(currency, time_frame,\n                                                                        from_timestamp, to_timestamp,\n                                                                        exchange))\n        except KeyError:\n            return util.get_rest_reply(\"No exchange portfolio\", 404)\n\n\n    @blueprint.route(\"/pnl_history\", methods=['GET'])\n    @login.login_required_when_activated\n    def pnl_history():\n        exchange = flask.request.args.get(\"exchange\")\n        symbol = flask.request.args.get(\"symbol\")\n        quote = flask.request.args.get(\"quote\")\n        since = flask.request.args.get(\"since\")\n        scale = flask.request.args.get(\"scale\", \"\")\n        return flask.jsonify(\n            models.get_pnl_history(\n                exchange=exchange,\n                quote=quote,\n                symbol=symbol,\n                since=since,\n                scale=scale,\n            )\n        )\n\n\n    @blueprint.route(\"/clear_orders_history\", methods=['POST'])\n    @login.login_required_when_activated\n    def clear_orders_history():\n        return util.get_rest_reply(models.clear_exchanges_orders_history())\n\n\n    @blueprint.route(\"/clear_trades_history\", methods=['POST'])\n    @login.login_required_when_activated\n    def clear_trades_history():\n        return util.get_rest_reply(models.clear_exchanges_trades_history())\n\n\n    @blueprint.route(\"/clear_portfolio_history\", methods=['POST'])\n    @login.login_required_when_activated\n    def clear_portfolio_history():\n        return flask.jsonify(models.clear_exchanges_portfolio_history())\n\n\n    @blueprint.route(\"/clear_transactions_history\", methods=['POST'])\n    @login.login_required_when_activated\n    def clear_transactions_history():\n        return flask.jsonify(models.clear_exchanges_transactions_history())\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/user_commands.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_services.api as services_api\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport octobot_services.interfaces.util as interfaces_util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/user_command\", methods=['POST'])\n    @login.login_required_when_activated\n    def user_command():\n        request_data = flask.request.get_json()\n        interfaces_util.run_in_bot_main_loop(\n            services_api.send_user_command(\n                interfaces_util.get_bot_api().get_bot_id(),\n                request_data[\"subject\"],\n                request_data[\"action\"],\n                request_data[\"data\"]\n            )\n        )\n        return flask.jsonify(request_data)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/api/webhook.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.logging as logging\n\n\n_WEBHOOKS_CALLBACKS = []\n\n\ndef register_webhook(callback):\n    _WEBHOOKS_CALLBACKS.append(callback)\n\n\ndef has_webhook(callback):\n    return callback in _WEBHOOKS_CALLBACKS\n\n\ndef register(blueprint):\n    @blueprint.route(\"/webhook/<identifier>\", methods=['POST'])\n    def webhook(identifier):\n        try:\n            for callback in _WEBHOOKS_CALLBACKS:\n                try:\n                    callback(identifier)\n                except Exception as err:\n                    logging.get_logger(__name__).exception(err, True, f\"Error when calling webhook: {err}\")\n            return '', 200\n        except KeyError:\n            flask.abort(500)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/constants.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\n# utility URLs\n# top 250 sorted currencies (expects a page id at the end)\nCURRENCIES_LIST_URL = \"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250&page=\"\nALL_SYMBOLS_URL = \"https://api.coingecko.com/api/v3/coins/list\"\n\n# config keys\nCONFIG_WATCHED_SYMBOLS = \"watched_symbols\"\n\n# web interface keys\nGLOBAL_CONFIG_KEY = \"global_config\"\nEVALUATOR_CONFIG_KEY = \"evaluator_config\"\nTENTACLES_CONFIG_KEY = \"tentacle_config\"\nDEACTIVATE_OTHERS = \"deactivate_others\"\nTRADING_CONFIG_KEY = \"trading_config\"\nUPDATED_CONFIG_SEPARATOR = \"_\"\nACTIVATION_KEY = \"activation\"\nTENTACLE_CLASS_NAME = \"name\"\nSTARTUP_CONFIG_KEY = \"startup_config\"\n\n# backtesting\nBOT_TOOLS_BACKTESTING = \"backtesting\"\nBOT_TOOLS_BACKTESTING_SOURCE = \"backtesting_source\"\nBOT_PREPARING_BACKTESTING = \"preparing_backtesting\"\n\n# strategy optimizer\nBOT_TOOLS_STRATEGY_OPTIMIZER = \"strategy_optimizer\"\n\n# data collector\nBOT_TOOLS_DATA_COLLECTOR = \"data_collector\"\n\nPRODUCT_HUNT_ANNOUNCEMENT = \"product_hunt_announcement\"\nPRODUCT_HUNT_ANNOUNCEMENT_DAY = 1720594860  # Wednesday, July 10, 2024 7:01:00 AM UTC\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot.enums\n\nimport tentacles.Services.Interfaces.web_interface.controllers.octobot_authentication\nimport tentacles.Services.Interfaces.web_interface.controllers.community_authentication\nimport tentacles.Services.Interfaces.web_interface.controllers.backtesting\nimport tentacles.Services.Interfaces.web_interface.controllers.commands\nimport tentacles.Services.Interfaces.web_interface.controllers.about\nimport tentacles.Services.Interfaces.web_interface.controllers.community\nimport tentacles.Services.Interfaces.web_interface.controllers.configuration\nimport tentacles.Services.Interfaces.web_interface.controllers.tentacles_config\nimport tentacles.Services.Interfaces.web_interface.controllers.dashboard\nimport tentacles.Services.Interfaces.web_interface.controllers.errors\nimport tentacles.Services.Interfaces.web_interface.controllers.octobot_help\nimport tentacles.Services.Interfaces.web_interface.controllers.home\nimport tentacles.Services.Interfaces.web_interface.controllers.interface_settings\nimport tentacles.Services.Interfaces.web_interface.controllers.logs\nimport tentacles.Services.Interfaces.web_interface.controllers.medias\nimport tentacles.Services.Interfaces.web_interface.controllers.terms\nimport tentacles.Services.Interfaces.web_interface.controllers.trading\nimport tentacles.Services.Interfaces.web_interface.controllers.portfolio\nimport tentacles.Services.Interfaces.web_interface.controllers.profiles\nimport tentacles.Services.Interfaces.web_interface.controllers.automation\nimport tentacles.Services.Interfaces.web_interface.controllers.reboot\nimport tentacles.Services.Interfaces.web_interface.controllers.welcome\nimport tentacles.Services.Interfaces.web_interface.controllers.robots\nimport tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making\nimport tentacles.Services.Interfaces.web_interface.controllers.dsl\n\n\ndef register(blueprint, distribution: octobot.enums.OctoBotDistribution):\n    if distribution is octobot.enums.OctoBotDistribution.DEFAULT:\n        tentacles.Services.Interfaces.web_interface.controllers.community_authentication.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.backtesting.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.about.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.community.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.configuration.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.tentacles_config.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.dashboard.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.octobot_help.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.home.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.interface_settings.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.logs.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.trading.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.portfolio.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.profiles.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.automation.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.welcome.register(blueprint)\n        tentacles.Services.Interfaces.web_interface.controllers.dsl.register(blueprint)\n    elif distribution is octobot.enums.OctoBotDistribution.MARKET_MAKING:\n        tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.register(blueprint)\n    # common routes\n    tentacles.Services.Interfaces.web_interface.controllers.octobot_authentication.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.robots.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.reboot.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.terms.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.errors.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.commands.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.medias.register(blueprint)\n\n\n__all__ = [\n    \"register\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/about.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot.constants as constants\nimport octobot.disclaimer as disclaimer\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\"/about\")\n    @login.login_required_when_activated\n    def about():\n        return flask.render_template('about.html',\n                                     octobot_beta_program_form_url=constants.OCTOBOT_BETA_PROGRAM_FORM_URL,\n                                     beta_env_enabled_in_config=models.get_beta_env_enabled_in_config(),\n                                     metrics_enabled=models.get_metrics_enabled(),\n                                     disclaimer=disclaimer.DISCLAIMER)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/automation.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.logging as commons_logging\nimport octobot_commons.authentication as authentication\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport octobot.automation as bot_automation\nimport octobot.constants as constants\n\n\ndef register(blueprint):\n    @blueprint.route(\"/automations\", methods=[\"POST\", \"GET\"])\n    @login.login_required_when_activated\n    def automations():\n        if not models.are_automations_enabled():\n            return flask.redirect(flask.url_for(\"home\"))\n        if flask.request.method == 'POST':\n            action = flask.request.args.get(\"action\")\n            success = True\n            response = \"\"\n            tentacle_name = bot_automation.Automation.get_name()\n            tentacle_class = bot_automation.Automation\n            restart = False\n            if action == \"save\":\n                request_data = flask.request.get_json()\n                success, response = models.update_tentacle_config(\n                    tentacle_name,\n                    request_data,\n                    tentacle_class=tentacle_class\n                )\n            if action == \"start\":\n                restart = True\n            elif action == \"factory_reset\":\n                success, response = models.reset_automation_config_to_default()\n                restart = True\n            if restart:\n                models.restart_global_automations()\n            if success:\n                return util.get_rest_reply(flask.jsonify(response))\n            else:\n                return util.get_rest_reply(response, 500)\n\n        display_intro = flask_util.BrowsingDataProvider.instance().get_and_unset_is_first_display(\n            flask_util.BrowsingDataProvider.AUTOMATIONS\n        )\n        all_events, all_conditions, all_actions = models.get_all_automation_steps()\n        form_to_display = constants.AUTOMATION_FEEDBACK_FORM_ID\n        try:\n            user_id = models.get_user_account_id()\n            display_feedback_form = models.has_at_least_one_running_automation() and not models.has_filled_form(form_to_display)\n        except authentication.AuthenticationRequired:\n            # no authenticated user: don't display form\n            user_id = None\n            display_feedback_form = False\n        return flask.render_template(\n            'automations.html',\n            profile_name=models.get_current_profile().name,\n            events=all_events,\n            conditions=all_conditions,\n            actions=all_actions,\n            display_intro=display_intro,\n            user_id=user_id,\n            form_to_display=form_to_display,\n            display_feedback_form=display_feedback_form,\n        )\n\n\n    @blueprint.route('/automations_edit_details')\n    @login.login_required_when_activated\n    def automations_edit_details():\n        if not models.are_automations_enabled():\n            return flask.redirect(flask.url_for(\"home\"))\n        try:\n            return util.get_rest_reply(\n                models.get_tentacle_config_and_edit_display(\n                    bot_automation.Automation.get_name(),\n                    tentacle_class=bot_automation.Automation\n                )\n            )\n        except Exception as e:\n            commons_logging.get_logger(\"automations_edit_details\").exception(e)\n            return util.get_rest_reply(str(e), 500)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/backtesting.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport werkzeug\n\nimport octobot_commons.time_frame_manager as time_frame_manager\n\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.errors as errors\n\n\ndef register(blueprint):\n    @blueprint.route(\"/backtesting\")\n    @blueprint.route('/backtesting', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def backtesting():\n        if not models.is_backtesting_enabled():\n            return flask.redirect(flask.url_for(\"home\"))\n        if flask.request.method == 'POST':\n            try:\n                action_type = flask.request.args[\"action_type\"]\n                success = False\n                reply = \"Action failed\"\n                if action_type == \"start_backtesting\":\n                    data = flask.request.get_json()\n                    source = flask.request.args[\"source\"]\n                    auto_stop = flask.request.args.get(\"auto_stop\", False)\n                    run_on_common_part_only = flask.request.args.get(\"run_on_common_part_only\", \"true\") == \"true\"\n                    reset_tentacle_config = flask.request.args.get(\"reset_tentacle_config\", False)\n                    success, reply = models.start_backtesting_using_specific_files(\n                        data[\"files\"],\n                        source,\n                        reset_tentacle_config,\n                        run_on_common_part_only,\n                        start_timestamp=data.get(\"start_timestamp\", None),\n                        end_timestamp=data.get(\"end_timestamp\", None),\n                        enable_logs=data.get(\"enable_logs\", False),\n                        auto_stop=auto_stop,\n                        collector_start_callback=web_interface.send_data_collector_status,\n                        start_callback=web_interface.send_backtesting_status)\n                elif action_type == \"start_backtesting_with_current_bot_data\":\n                    data = flask.request.get_json()\n                    source = flask.request.args[\"source\"]\n                    auto_stop = flask.request.args.get(\"auto_stop\", False)\n                    exchange_id = data.get(\"exchange_id\", None)\n                    trading_type = data.get(\"exchange_type\", None)\n                    profile_id = data.get(\"profile_id\", None)\n                    name = data.get(\"name\", None)\n                    reset_tentacle_config = flask.request.args.get(\"reset_tentacle_config\", False)\n                    success, reply = models.start_backtesting_using_current_bot_data(\n                        data.get(\"data_source\", models.CURRENT_BOT_DATA),\n                        exchange_id,\n                        source,\n                        reset_tentacle_config,\n                        start_timestamp=data.get(\"start_timestamp\", None),\n                        end_timestamp=data.get(\"end_timestamp\", None),\n                        trading_type=trading_type,\n                        profile_id=profile_id,\n                        enable_logs=data.get(\"enable_logs\", False),\n                        auto_stop=auto_stop,\n                        name=name,\n                        collector_start_callback=web_interface.send_data_collector_status,\n                        start_callback=web_interface.send_backtesting_status\n                    )\n                elif action_type == \"stop_backtesting\":\n                    success, reply = models.stop_previous_backtesting()\n                if success:\n                    return util.get_rest_reply(flask.jsonify(reply))\n                else:\n                    return util.get_rest_reply(reply, 500)\n\n            except errors.MissingExchangeId:\n                return util.get_rest_reply(errors.MissingExchangeId.EXPLANATION, 500)\n\n        elif flask.request.method == 'GET':\n            if flask.request.args:\n                target = flask.request.args[\"update_type\"]\n                if target == \"backtesting_report\":\n                    source = flask.request.args[\"source\"]\n                    backtesting_report = models.get_backtesting_report(source)\n                    return flask.jsonify(backtesting_report)\n\n            else:\n                return flask.render_template('backtesting.html',\n                                             activated_trading_mode=models.get_config_activated_trading_mode(),\n                                             data_files=models.get_data_files_with_description())\n\n\n    @blueprint.route(\"/backtesting_run_id\")\n    @login.login_required_when_activated\n    def backtesting_run_id():\n        trading_mode = models.get_config_activated_trading_mode()\n        run_id = models.get_latest_backtesting_run_id(trading_mode)\n        return flask.jsonify(run_id)\n\n\n    @blueprint.route(\"/data_collector\")\n    @blueprint.route('/data_collector', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def data_collector():\n        if not models.is_backtesting_enabled():\n            return flask.redirect(flask.url_for(\"home\"))\n        if flask.request.method == 'POST':\n            action_type = flask.request.args[\"action_type\"]\n            success = False\n            reply = \"Action failed\"\n            if action_type == \"delete_data_file\":\n                file = flask.request.get_json()\n                success, reply = models.get_delete_data_file(file)\n            elif action_type == \"start_collector\":\n                details = flask.request.get_json()\n                success, reply = models.collect_data_file(details[\"exchange\"], details[\"symbols\"], details[\"time_frames\"],\n                                                          details[\"startTimestamp\"], details[\"endTimestamp\"])\n                if success:\n                    web_interface.send_data_collector_status()\n            elif action_type == \"stop_collector\":\n                success, reply = models.stop_data_collector()\n            elif action_type == \"import_data_file\":\n                if flask.request.files:\n                    file = flask.request.files['file']\n                    name = werkzeug.utils.secure_filename(flask.request.files['file'].filename)\n                    success, reply = models.save_data_file(name, file)\n                    alert = {\"success\": success, \"message\": reply}\n                else:\n                    alert = {}\n                current_exchange = models.get_current_exchange()\n\n                # here return template to force page reload because of file upload via input form\n                return flask.render_template('data_collector.html',\n                                             data_files=models.get_data_files_with_description(),\n                                             other_ccxt_exchanges=sorted(models.get_other_history_exchange_list()),\n                                             full_candle_history_ccxt_exchanges=models.get_full_candle_history_exchange_list(),\n                                             current_exchange=models.get_current_exchange(),\n                                             full_symbol_list=sorted(models.get_symbol_list([current_exchange])),\n                                             available_timeframes_list=[timeframe.value for timeframe in\n                                                                        time_frame_manager.sort_time_frames(\n                                                                            models.get_timeframes_list([current_exchange]))],\n                                             alert=alert)\n            if success:\n                return util.get_rest_reply(flask.jsonify(reply))\n            else:\n                return util.get_rest_reply(reply, 500)\n\n        elif flask.request.method == 'GET':\n            origin_page = None\n            if flask.request.args:\n                action_type_key = \"action_type\"\n                if action_type_key in flask.request.args:\n                    target = flask.request.args[action_type_key]\n                    if target == \"symbol_list\":\n                        exchange = flask.request.args.get('exchange')\n                        return flask.jsonify(sorted(models.get_symbol_list([exchange])))\n                    elif target == \"available_timeframes_list\":\n                        exchange = flask.request.args.get('exchange')\n                        return flask.jsonify([timeframe.value for timeframe in\n                                              time_frame_manager.sort_time_frames(\n                                                  models.get_timeframes_list([exchange]))])\n                from_key = \"from\"\n                if from_key in flask.request.args:\n                    origin_page = flask.request.args[from_key]\n\n            current_exchange = models.get_current_exchange()\n            return flask.render_template('data_collector.html',\n                                         data_files=models.get_data_files_with_description(),\n                                         other_ccxt_exchanges=sorted(models.get_other_history_exchange_list()),\n                                         full_candle_history_ccxt_exchanges=models.get_full_candle_history_exchange_list(),\n                                         current_exchange=models.get_current_exchange(),\n                                         full_symbol_list=sorted(models.get_symbol_list([current_exchange])),\n                                         available_timeframes_list=[timeframe.value for timeframe in\n                                                                    time_frame_manager.sort_time_frames(\n                                                                        models.get_timeframes_list([current_exchange]))],\n                                         origin_page=origin_page,\n                                         alert={})\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/commands.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route('/commands/<cmd>', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def commands(cmd=None):\n        if cmd == \"restart\":\n            models.schedule_delayed_command(models.restart_bot, delay=0.1)\n            return flask.jsonify(\"Success\")\n\n        elif cmd == \"stop\":\n            models.schedule_delayed_command(models.stop_bot, delay=0.1)\n            return flask.jsonify(\"Success\")\n\n        elif cmd == \"update\":\n            models.schedule_delayed_command(models.update_bot, delay=0.1)\n            return flask.jsonify(\"Update started\")\n\n        else:\n            raise RuntimeError(\"Unknown command\")\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/community.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.authentication as authentication\nimport octobot.constants as constants\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot.community.errors\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\"/community\")\n    @login.login_required_when_activated\n    def community():\n        authenticator = authentication.Authenticator.instance()\n        logged_in_email = None\n        use_preview = not authenticator.can_authenticate()\n        all_user_bots = []\n        try:\n            models.wait_for_login_if_processing()\n            logged_in_email = authenticator.get_logged_in_email()\n            all_user_bots = models.get_all_user_bots()\n        except authentication.AuthenticationError as err:\n            # force logout and redirect to login\n            flask.flash(f\"Your session expired, please re-authenticate to your account.\", \"error\")\n            interfaces_util.run_in_bot_main_loop(authentication.Authenticator.instance().logout())\n            return flask.redirect('community_login')\n        except (authentication.AuthenticationRequired, authentication.UnavailableError):\n            # not authenticated\n            pass\n        except Exception as e:\n            flask.flash(f\"Error when contacting the community server: {e}\", \"error\")\n        if logged_in_email is None and not use_preview:\n            return flask.redirect('community_login')\n        strategies = models.get_cloud_strategies(authenticator)\n        return flask.render_template(\n            'community.html',\n            current_logged_in_email=logged_in_email,\n            role=authenticator.user_account.supports.support_role,\n            is_donor=bool(authenticator.user_account.supports.is_donor()),\n            strategies=strategies,\n            current_bots_stats=models.get_current_octobots_stats(),\n            all_user_bots=all_user_bots,\n            selected_user_bot=models.get_selected_user_bot(),\n            can_logout=models.can_logout(),\n            can_select_bot=models.can_select_bot(),\n            has_owned_packages_to_install=models.has_owned_packages_to_install(),\n        )\n\n\n    @blueprint.route(\"/community_metrics\")\n    @login.login_required_when_activated\n    def community_metrics():\n        return flask.redirect(\"/\")\n        can_get_metrics = models.can_get_community_metrics()\n        display_metrics = models.get_community_metrics_to_display() if can_get_metrics else None\n        return flask.render_template('community_metrics.html',\n                                     can_get_metrics=can_get_metrics,\n                                     community_metrics=display_metrics\n                                     )\n\n    @blueprint.route(\"/extensions\")\n    @login.login_required_when_activated\n    def extensions():\n        refresh_packages = flask.request.args.get(\"refresh_packages\") if flask.request.args else \"false\"\n        loop = flask.request.args.get(\"loop\") if flask.request.args else \"false\"\n        authenticator = authentication.Authenticator.instance()\n        logged_in_email = None\n        try:\n            models.wait_for_login_if_processing()\n            logged_in_email = authenticator.get_logged_in_email()\n            if refresh_packages.lower() == \"true\":\n                models.update_owned_packages()\n        except (authentication.AuthenticationRequired, authentication.UnavailableError, authentication.AuthenticationError):\n            pass\n        except Exception as e:\n            flask.flash(f\"Error when contacting the community server: {e}\", \"error\")\n        return flask.render_template(\n            'extensions.html',\n            current_logged_in_email=logged_in_email,\n            is_community_authenticated=logged_in_email is not None,\n            price=constants.OCTOBOT_EXTENSION_PACKAGE_1_PRICE,\n            auto_refresh_packages=refresh_packages and loop == \"true\",\n            has_owned_packages_to_install=models.has_owned_packages_to_install(),\n        )\n\n    @blueprint.route(\"/tradingview_email_config\")\n    @login.login_required_when_activated\n    def tradingview_email_config():\n        models.wait_for_login_if_processing()\n        return flask.render_template(\n            'tradingview_email_config.html',\n            is_community_authenticated=authentication.Authenticator.instance().is_logged_in(),\n            tradingview_email_address=models.get_tradingview_email_address(),\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/community_authentication.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport flask_wtf\nimport wtforms.fields\n\nimport octobot.community.errors as community_errors\nimport octobot_commons.authentication as authentication\nimport octobot_commons.logging as logging\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\nVALIDATE_EMAIL_INFO = \"Please validate your email from the confirm link we sent you and re-enter your credentials.\"\n\n\ndef register(blueprint):\n    @blueprint.route('/community_login', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def community_login():\n        next_url = flask.request.args.get(\"next\", None)\n        after_login_action = flask.request.args.get(\"after_login_action\", None)\n        authenticator = authentication.Authenticator.instance()\n        logged_in_email = None\n        form = CommunityLoginForm(flask.request.form) if flask.request.form else CommunityLoginForm()\n        try:\n            logged_in_email = authenticator.get_logged_in_email()\n        except authentication.AuthenticationRequired:\n            pass\n        except Exception as e:\n            flask.flash(f\"Error when contacting the community server: {e}\", \"error\")\n        if logged_in_email is None:\n            if form.validate_on_submit():\n                try:\n                    interfaces_util.run_in_bot_main_loop(\n                        authenticator.login(form.email.data, form.password.data),\n                        log_exceptions=False\n                    )\n                    logged_in_email = form.email.data\n                    if after_login_action == \"sync_account\":\n                        added_profiles = models.sync_community_account()\n                        if added_profiles:\n                            flask.flash(f\"Downloaded {len(added_profiles)} profile{'s' if len(added_profiles) > 1 else ''} \"\n                                        f\"from your OctoBot account.\", \"success\")\n                except community_errors.EmailValidationRequiredError:\n                    flask.flash(VALIDATE_EMAIL_INFO, \"info\")\n                except authentication.FailedAuthentication as err:\n                    flask.flash(str(err), \"error\")\n                except Exception as e:\n                    logging.get_logger(\"CommunityAuthentication\").exception(e, False)\n                    flask.flash(f\"Error during authentication: {e}\", \"error\")\n        if flask.request.method == 'POST' and next_url and authenticator.is_logged_in():\n            return flask.redirect(next_url)\n        return flask.render_template('community_login.html',\n                                     form=form,\n                                     current_logged_in_email=logged_in_email,\n                                     current_bots_stats=models.get_current_octobots_stats(),\n                                     next_url=next_url or flask.url_for('community'))\n\n\n    @blueprint.route('/community_register', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def community_register():\n        if not models.can_logout():\n            return flask.redirect(flask.url_for('community'))\n        next_url = flask.request.args.get(\"next\", None)\n        after_login_action = flask.request.args.get(\"after_login_action\", None)\n        authenticator = authentication.Authenticator.instance()\n        form = CommunityLoginForm(flask.request.form) if flask.request.form else CommunityLoginForm()\n        logged_in_email = None\n        if form.validate_on_submit():\n            try:\n                interfaces_util.run_in_bot_main_loop(\n                    authenticator.register(form.email.data, form.password.data),\n                    log_exceptions=False\n                )\n                logged_in_email = form.email.data\n                if after_login_action == \"sync_account\":\n                    added_profiles = models.sync_community_account()\n                    if added_profiles:\n                        flask.flash(f\"Downloaded {len(added_profiles)} profile{'s' if len(added_profiles) > 1 else ''} \"\n                                    f\"from your OctoBot account.\", \"success\")\n                # creation success: redirect to next_url\n                if next_url:\n                    return flask.redirect(next_url)\n            except community_errors.EmailValidationRequiredError:\n                flask.flash(VALIDATE_EMAIL_INFO, \"info\")\n                interfaces_util.run_in_bot_main_loop(authenticator.logout())\n                return flask.redirect(flask.url_for(f\"community_login\", **flask.request.args))\n            except authentication.AuthenticationError as err:\n                flask.flash(str(err), \"error\")\n                interfaces_util.run_in_bot_main_loop(authenticator.logout())\n            except Exception as e:\n                logging.get_logger(\"CommunityAuthentication\").exception(e, False)\n                flask.flash(f\"Unexpected error when creating account: {e}\", \"error\")\n                interfaces_util.run_in_bot_main_loop(authenticator.logout())\n        return flask.render_template('community_register.html',\n                                     form=form,\n                                     current_logged_in_email=logged_in_email,\n                                     current_bots_stats=models.get_current_octobots_stats(),\n                                     next_url=next_url or flask.url_for('community'))\n\n\n    @blueprint.route(\"/community_logout\")\n    @login.login_required_when_activated\n    def community_logout():\n        next_url = flask.request.args.get(\"next\", flask.url_for(\"community_login\"))\n        if not models.can_logout():\n            return flask.redirect(flask.url_for('community'))\n        interfaces_util.run_in_bot_main_loop(authentication.Authenticator.instance().logout())\n        return flask.redirect(next_url)\n\n\nclass CommunityLoginForm(flask_wtf.FlaskForm):\n    email = wtforms.fields.EmailField('Email', [wtforms.validators.InputRequired()])\n    password = wtforms.PasswordField('Password', [wtforms.validators.InputRequired()])\n    remember_me = wtforms.BooleanField('Remember me', default=True)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/configuration.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport werkzeug\nimport os\nfrom datetime import datetime\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.authentication as authentication\nimport octobot_services.constants as services_constants\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport octobot_backtesting.api as backtesting_api\nimport octobot_trading.api as trading_api\nimport octobot_services.interfaces.util as interfaces_util\n\n\ndef register(blueprint):\n    @blueprint.route('/profile')\n    @login.login_required_when_activated\n    def profile():\n        selected_profile = flask.request.args.get(\"select\", None)\n        next_url = flask.request.args.get(\"next\", None)\n        if selected_profile is not None and selected_profile != models.get_current_profile().profile_id:\n            models.select_profile(selected_profile)\n            current_profile = models.get_current_profile()\n            flask.flash(\n                f\"Selected the {current_profile.name} profile\", \"success\"\n            )\n        else:\n            current_profile = models.get_current_profile()\n        if next_url is not None:\n            return flask.redirect(next_url)\n        media_url = flask.url_for(\"tentacle_media\", _external=True)\n        display_config = interfaces_util.get_edited_config()\n\n        missing_tentacles = set()\n        profiles = models.get_profiles(commons_enums.ProfileType.LIVE)\n        config_exchanges = display_config[commons_constants.CONFIG_EXCHANGES]\n        enabled_exchange_types = models.get_enabled_exchange_types(config_exchanges)\n        enabled_exchanges = trading_api.get_enabled_exchanges_names(display_config)\n        display_intro = flask_util.BrowsingDataProvider.instance().get_and_unset_is_first_display(\n            flask_util.BrowsingDataProvider.PROFILE\n        )\n        exchange_symbols = sorted(models.get_symbol_list(enabled_exchanges or config_exchanges))\n        config_symbols = models.format_config_symbols(display_config)\n        return flask.render_template(\n            'profile.html',\n             current_profile=current_profile,\n             profiles=profiles,\n             profiles_tentacles_details=models.get_profiles_tentacles_details(profiles),\n             display_intro=display_intro,\n\n             config_exchanges=config_exchanges,\n             enabled_exchange_types=enabled_exchange_types,\n             config_trading=display_config[commons_constants.CONFIG_TRADING],\n             config_trader=display_config[commons_constants.CONFIG_TRADER],\n             config_trader_simulator=display_config[commons_constants.CONFIG_SIMULATOR],\n             config_symbols=config_symbols,\n             config_reference_market=display_config[commons_constants.CONFIG_TRADING][\n                 commons_constants.CONFIG_TRADER_REFERENCE_MARKET],\n\n             real_trader_activated=interfaces_util.has_real_and_or_simulated_traders()[0],\n\n             symbol_list_by_type=models.get_all_symbols_list_by_symbol_type(exchange_symbols, config_symbols),\n             full_symbol_list=models.get_all_symbols_list(),\n             evaluator_config=models.get_evaluator_detailed_config(media_url, missing_tentacles),\n             strategy_config=models.get_strategy_config(media_url, missing_tentacles),\n             evaluator_startup_config=models.get_evaluators_tentacles_startup_activation(),\n             trading_startup_config=models.get_trading_tentacles_startup_activation(),\n             missing_tentacles=missing_tentacles,\n\n             in_backtesting=backtesting_api.is_backtesting_enabled(display_config),\n\n             other_tentacles_config=models.get_extra_tentacles_config_desc(media_url,\n                                                                           missing_tentacles),\n\n             config_tentacles_by_group=models.get_tentacles_activation_desc_by_group(media_url,\n                                                                                     missing_tentacles),\n\n             exchanges_details=models.get_exchanges_details(config_exchanges),\n\n             are_automations_enabled=models.are_automations_enabled(),\n             automations_count=models.get_automations_count(),\n        )\n\n\n    @blueprint.route('/profiles_management/<action>', methods=[\"POST\", \"GET\"])\n    @login.login_required_when_activated\n    def profiles_management(action):\n        if action == \"update\":\n            data = flask.request.get_json()\n            success, err = models.update_profile(flask.request.get_json()[\"id\"], data)\n            if not success:\n                return util.get_rest_reply(flask.jsonify(str(err)), code=400)\n            return util.get_rest_reply(flask.jsonify(data))\n        if action == \"duplicate\":\n            profile_id = flask.request.args.get(\"profile_id\")\n            models.duplicate_profile(profile_id)\n            flask.flash(f\"New profile successfully created.\", \"success\")\n            return util.get_rest_reply(flask.jsonify(\"Profile created\"))\n        if action == \"use_as_live\":\n            profile_id = flask.request.args.get(\"profile_id\")\n            models.convert_to_live_profile(profile_id)\n            models.select_profile(profile_id)\n            flask.flash(f\"Profile successfully converted to live profile and selected.\", \"success\")\n            return flask.redirect(flask.url_for(\"profile\"))\n        if action == \"remove\":\n            data = flask.request.get_json()\n            to_remove_id = data[\"id\"]\n            removed_profile, err = models.remove_profile(to_remove_id)\n            if err is not None:\n                return util.get_rest_reply(flask.jsonify(str(err)), code=400)\n            flask.flash(f\"{removed_profile.name} profile removed.\", \"success\")\n            return util.get_rest_reply(flask.jsonify(\"Profile created\"))\n        next_url = flask.request.args.get(\"next\", flask.url_for('profile'))\n        if action == \"import\":\n            file = flask.request.files['file']\n            name = werkzeug.utils.secure_filename(flask.request.files['file'].filename)\n            try:\n                new_profile = models.import_profile(file, name)\n                flask.flash(f\"{new_profile.name} profile successfully imported.\", \"success\")\n            except Exception as err:\n                flask.flash(f\"Error when importing profile: {err}.\", \"danger\")\n            return flask.redirect(next_url)\n        if action == \"download\":\n            url = flask.request.form.get('inputProfileLink')\n            strategy_id = flask.request.json.get('strategy_id')\n            name = flask.request.json.get('name')\n            description = flask.request.json.get('description')\n            profile_id = \"\"\n            try:\n                if url:\n                    new_profile = models.download_and_import_profile(url)\n                else:\n                    if None in (strategy_id, name):\n                        raise RuntimeError(\"Both strategy_id and name are required to import a strategy\")\n                    authenticator = authentication.Authenticator.instance()\n                    strategy = models.get_cloud_strategy(authenticator, strategy_id)\n                    new_profile = models.import_strategy_as_profile(\n                        authenticator, strategy, name, description\n                    )\n                    profile_id = new_profile.profile_id\n                message = f\"{new_profile.name} profile successfully imported.\"\n                success = True\n            except FileNotFoundError:\n                message = f\"Invalid profile url {url}\"\n                success = False\n            except Exception as err:\n                message = f\"Error when importing profile: {err}\"\n                success = False\n            if flask.request.method == \"POST\":\n                return util.get_rest_reply(\n                    flask.jsonify({\"text\": message, \"profile_id\": profile_id}),\n                    code=200 if success else 400\n                )\n            flask.flash(f\"{message}\", \"success\" if success else \"danger\")\n            return flask.redirect(next_url)\n        if action == \"export\":\n            profile_id = flask.request.args.get(\"profile_id\")\n            temp_file = os.path.abspath(\"profile\")\n            file_path = models.export_profile(profile_id, temp_file)\n            name = models.get_profile_name(profile_id)\n            return flask_util.send_and_remove_file(file_path, f\"{name}_{datetime.now().strftime('%Y%m%d-%H%M%S')}.zip\")\n\n\n    @blueprint.route('/accounts')\n    @login.login_required_when_activated\n    def accounts():\n        display_config = interfaces_util.get_edited_config()\n\n        # service lists\n        service_list = models.get_services_list()\n        notifiers_list = models.get_notifiers_list()\n\n        config_exchanges = display_config[commons_constants.CONFIG_EXCHANGES]\n        return flask.render_template('accounts.html',\n                                     ccxt_tested_exchanges=models.get_tested_exchange_list(),\n                                     ccxt_simulated_tested_exchanges=models.get_simulated_exchange_list(),\n                                     ccxt_other_exchanges=sorted(models.get_other_exchange_list()),\n                                     exchanges_details=models.get_exchanges_details(config_exchanges),\n\n                                     config_exchanges=config_exchanges,\n                                     config_notifications=display_config[\n                                         services_constants.CONFIG_CATEGORY_NOTIFICATION],\n                                     config_services=display_config[services_constants.CONFIG_CATEGORY_SERVICES],\n\n                                     services_list=service_list,\n                                     notifiers_list=notifiers_list,\n                                     )\n\n\n    @blueprint.route('/config', methods=['POST'])\n    @login.login_required_when_activated\n    def config():\n        next_url = flask.request.args.get(\"next\", None)\n        request_data = flask.request.get_json()\n        success = True\n        response = \"\"\n        err_message = \"\"\n\n        if request_data:\n\n            # update trading config if required\n            if constants.TRADING_CONFIG_KEY in request_data and request_data[constants.TRADING_CONFIG_KEY]:\n                success = success and models.update_tentacles_activation_config(\n                    request_data[constants.TRADING_CONFIG_KEY])\n            else:\n                request_data[constants.TRADING_CONFIG_KEY] = \"\"\n\n            # update tentacles config if required\n            if constants.TENTACLES_CONFIG_KEY in request_data and request_data[constants.TENTACLES_CONFIG_KEY]:\n                success = success and models.update_tentacles_activation_config(\n                    request_data[constants.TENTACLES_CONFIG_KEY])\n            else:\n                request_data[constants.TENTACLES_CONFIG_KEY] = \"\"\n\n            # update evaluator config if required\n            if constants.EVALUATOR_CONFIG_KEY in request_data and request_data[constants.EVALUATOR_CONFIG_KEY]:\n                deactivate_others = False\n                if constants.DEACTIVATE_OTHERS in request_data:\n                    deactivate_others = request_data[constants.DEACTIVATE_OTHERS]\n                success = success and models.update_tentacles_activation_config(\n                    request_data[constants.EVALUATOR_CONFIG_KEY],\n                    deactivate_others)\n            else:\n                request_data[constants.EVALUATOR_CONFIG_KEY] = \"\"\n\n            # remove elements from global config if any to remove\n            removed_elements_key = \"removed_elements\"\n            if removed_elements_key in request_data and request_data[removed_elements_key]:\n                update_success, err_message = models.update_global_config(request_data[removed_elements_key], delete=True)\n                success = success and update_success\n            else:\n                request_data[removed_elements_key] = \"\"\n\n            # update global config if required\n            if constants.GLOBAL_CONFIG_KEY in request_data and request_data[constants.GLOBAL_CONFIG_KEY]:\n                success, err_message = models.update_global_config(request_data[constants.GLOBAL_CONFIG_KEY])\n            else:\n                request_data[constants.GLOBAL_CONFIG_KEY] = \"\"\n\n            response = {\n                \"evaluator_updated_config\": request_data[constants.EVALUATOR_CONFIG_KEY],\n                \"trading_updated_config\": request_data[constants.TRADING_CONFIG_KEY],\n                \"tentacle_updated_config\": request_data[constants.TENTACLES_CONFIG_KEY],\n                \"global_updated_config\": request_data[constants.GLOBAL_CONFIG_KEY],\n                removed_elements_key: request_data[removed_elements_key]\n            }\n\n        if success:\n            if request_data.get(\"restart_after_save\", False):\n                models.schedule_delayed_command(models.restart_bot)\n            if next_url is not None:\n                return flask.redirect(next_url)\n            return util.get_rest_reply(flask.jsonify(response))\n        else:\n            return util.get_rest_reply(flask.jsonify(err_message), 500)\n\n\n    @blueprint.route('/metrics_settings', methods=['POST'])\n    @login.login_required_when_activated\n    def metrics_settings():\n        return util.get_rest_reply(flask.jsonify(models.activate_metrics(flask.request.get_json())))\n\n\n    @blueprint.route('/beta_env_settings', methods=['POST'])\n    @login.login_required_when_activated\n    def beta_env_settings():\n        return util.get_rest_reply(flask.jsonify(models.activate_beta_env(flask.request.get_json())))\n\n\n    @blueprint.route('/config_actions', methods=['POST'])\n    @login.login_required_when_activated\n    def config_actions():\n        # action = flask.request.args.get(\"action\")\n        return util.get_rest_reply(\"No specified action.\", code=500)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/dashboard.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\n        '/dashboard/currency_price_graph_update/<exchange_id>/<symbol>/<time_frame>/<mode>')\n    @login.login_required_when_activated\n    def currency_price_graph_update(exchange_id, symbol, time_frame, mode=\"live\"):\n        in_backtesting = mode != \"live\"\n        display_orders = flask.request.args.get(\"display_orders\", \"true\") == \"true\"\n        return flask.jsonify(models.get_currency_price_graph_update(exchange_id,\n                                                                    models.get_value_from_dict_or_string(symbol),\n                                                                    time_frame,\n                                                                    backtesting=in_backtesting,\n                                                                    ignore_orders=not display_orders))\n\n\n    @blueprint.route('/dashboard/first_symbol')\n    @login.login_required_when_activated\n    def first_symbol():\n        return flask.jsonify(models.get_first_symbol_data())\n\n\n    @blueprint.route('/dashboard/watched_symbol/<symbol>')\n    @login.login_required_when_activated\n    def watched_symbol(symbol):\n        return flask.jsonify(models.get_watched_symbol_data(symbol))\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/distributions/__init__.py",
    "content": ""
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/distributions/market_making/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Services.Interfaces.web_interface.controllers.portfolio\nimport tentacles.Services.Interfaces.web_interface.controllers.logs\nimport tentacles.Services.Interfaces.web_interface.controllers.dashboard\nimport tentacles.Services.Interfaces.web_interface.controllers.tentacles_config\nimport tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.dashboard\nimport tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.configuration\nimport tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.cloud\n\n\ndef register(blueprint):\n    tentacles.Services.Interfaces.web_interface.controllers.portfolio.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.logs.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.dashboard.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.tentacles_config.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.dashboard.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.configuration.register(blueprint)\n    tentacles.Services.Interfaces.web_interface.controllers.distributions.market_making.cloud.register(blueprint)\n\n\n__all__ = [\n    \"register\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/distributions/market_making/cloud.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/cloud\")\n    @login.login_required_when_activated\n    def cloud():\n        return flask.render_template(\n            'distributions/market_making/cloud.html',\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/distributions/market_making/configuration.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.logging as commons_logging\nimport octobot_services.constants as services_constants\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport octobot_trading.api as trading_api\n\n\ndef register(blueprint):\n    @blueprint.route('/configuration')\n    @login.login_required_when_activated\n    def configuration():\n        display_intro = flask_util.BrowsingDataProvider.instance().get_and_unset_is_first_display(\n            flask_util.BrowsingDataProvider.get_distribution_key(\n                models.get_distribution(),\n                flask_util.BrowsingDataProvider.CONFIGURATION,\n            )\n        )\n        display_config = interfaces_util.get_edited_config()\n        enabled_exchanges = trading_api.get_enabled_exchanges_names(display_config)\n        config_symbols = models.get_enabled_trading_pairs()\n        first_symbol_pair = next(iter(config_symbols)) if config_symbols else []\n        config_exchanges = models.get_json_exchange_config(display_config)\n        trading_mode = models.get_config_activated_trading_mode()\n        media_url = flask.url_for(\"tentacle_media\", _external=True)\n        tentacle_docs = \"\"\n        trading_mode_name = trading_mode.get_name() if trading_mode else \"Missing trading mode\"\n        if trading_mode:\n            tentacle_docs = models.get_tentacle_documentation(trading_mode.get_name(), media_url)\n        return flask.render_template(\n            'distributions/market_making/configuration.html',\n            selected_exchange=enabled_exchanges[0] if enabled_exchanges else (config_exchanges[0][models.NAME] if config_exchanges else None),\n            config_exchanges=config_exchanges,\n            exchanges_schema=models.get_json_exchanges_schema(models.get_tested_exchange_list()),\n\n            selected_pair=first_symbol_pair,\n\n            trading_mode_name=trading_mode_name,\n            tentacle_docs=tentacle_docs,\n\n            simulated_portfolio=models.get_json_simulated_portfolio(display_config),\n            portfolio_schema=models.JSON_PORTFOLIO_SCHEMA,\n            trading_simulator_schema=models.JSON_TRADING_SIMULATOR_SCHEMA,\n            config_trading_simulator=models.get_json_trading_simulator_config(display_config),\n\n            display_intro=display_intro,\n        )\n\n    @blueprint.route('/interfaces')\n    @login.login_required_when_activated\n    def interfaces():\n        display_config = interfaces_util.get_edited_config()\n\n        # service lists\n        service_list = models.get_market_making_services()\n        services_config = {\n            service: config\n            for service, config in display_config[services_constants.CONFIG_CATEGORY_SERVICES].items()\n            if service in service_list\n        }\n        notifiers_list = models.get_notifiers_list()\n\n        return flask.render_template(\n            'distributions/market_making/interfaces.html',\n            config_notifications=display_config[\n             services_constants.CONFIG_CATEGORY_NOTIFICATION],\n            config_services=services_config,\n\n            services_list=service_list,\n            notifiers_list=notifiers_list,\n        )\n\n\n\n    @blueprint.route('/interface_config', methods=['POST'])\n    @login.login_required_when_activated\n    def interface_config():\n        next_url = flask.request.args.get(\"next\", None)\n        request_data = flask.request.get_json()\n        success = True\n        response = \"\"\n        err_message = \"\"\n\n        if request_data:\n            # remove elements from global config if any to remove\n            removed_elements_key = \"removed_elements\"\n            if removed_elements_key in request_data and request_data[removed_elements_key]:\n                update_success, err_message = models.update_global_config(request_data[removed_elements_key], delete=True)\n                success = success and update_success\n            else:\n                request_data[removed_elements_key] = \"\"\n\n            # update global config if required\n            if constants.GLOBAL_CONFIG_KEY in request_data and request_data[constants.GLOBAL_CONFIG_KEY]:\n                success, err_message = models.update_global_config(request_data[constants.GLOBAL_CONFIG_KEY])\n            else:\n                request_data[constants.GLOBAL_CONFIG_KEY] = \"\"\n\n            response = {\n                \"global_updated_config\": request_data[constants.GLOBAL_CONFIG_KEY],\n                removed_elements_key: request_data[removed_elements_key]\n            }\n\n        if success:\n            if request_data.get(\"restart_after_save\", False):\n                models.schedule_delayed_command(models.restart_bot)\n            if next_url is not None:\n                return flask.redirect(next_url)\n            return util.get_rest_reply(flask.jsonify(response))\n        else:\n            return util.get_rest_reply(flask.jsonify(err_message), 500)\n\n    @blueprint.route('/configuration', methods=['POST'])\n    @login.login_required_when_activated\n    def save_market_making_config():\n        request_data = flask.request.get_json()\n        success = False\n        response = \"Restart to apply.\"\n        err_message = None\n        try:\n            models.save_market_making_configuration(\n                request_data[\"exchange\"],\n                request_data[\"tradingPair\"],\n                request_data[\"exchangesConfig\"],\n                request_data[\"tradingSimulatorConfig\"],\n                request_data[\"simulatedPortfolioConfig\"],\n                request_data[\"tradingModeName\"],\n                request_data[\"tradingModeConfig\"],\n            )\n            success = True\n        except Exception as e:\n            err_message = f\"Failed to save market making configuration: {e.__class__.__name__}: {e}\"\n            commons_logging.get_logger(\"save_market_making_config\").exception(\n                e, True, f\"{err_message} ({e.__class__.__name__})\"\n            )\n        if success:\n            return util.get_rest_reply(flask.jsonify(response))\n        else:\n            return util.get_rest_reply(flask.jsonify(err_message), 500)\n\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/distributions/market_making/dashboard.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport time\nimport flask\n\nimport octobot_commons.authentication as authentication\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport tentacles.Services.Interfaces.web_interface.constants as web_constants\nimport octobot.constants as constants\nimport octobot_commons.constants\nimport octobot_commons.enums\n\n\ndef register(blueprint):\n    @blueprint.route(\"/\")\n    @blueprint.route(\"/home\")\n    @login.login_required_when_activated\n    def home():\n        if flask.request.args.get(\"reset_tutorials\", \"False\").lower() == \"true\":\n            flask_util.BrowsingDataProvider.instance().set_first_displays(True)\n        if models.accepted_terms():\n            display_intro = flask_util.BrowsingDataProvider.instance().get_and_unset_is_first_display(\n                flask_util.BrowsingDataProvider.get_distribution_key(\n                    models.get_distribution(),\n                    flask_util.BrowsingDataProvider.HOME,\n                )\n            )\n            all_time_frames = models.get_all_watched_time_frames()\n            display_time_frame = models.get_display_timeframe()\n            display_orders = models.get_display_orders()\n            sandbox_exchanges = models.get_sandbox_exchanges()\n            past_launch_time = (\n                web_constants.PRODUCT_HUNT_ANNOUNCEMENT_DAY\n                + (\n                    octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames.ONE_DAY]\n                    * octobot_commons.constants.MINUTE_TO_SECONDS\n                )\n            )\n            is_launching = (\n               web_constants.PRODUCT_HUNT_ANNOUNCEMENT_DAY\n               <= time.time()\n               <= past_launch_time\n            )\n\n            display_ph_launch = (\n                models.get_display_announcement(web_constants.PRODUCT_HUNT_ANNOUNCEMENT) or is_launching\n            ) and not time.time() > past_launch_time\n            return flask.render_template(\n                'distributions/market_making/dashboard.html',\n                display_intro=display_intro,\n                reference_unit=interfaces_util.get_reference_market(),\n                display_time_frame=display_time_frame,\n                display_orders=display_orders,\n                all_time_frames=all_time_frames,\n                sandbox_exchanges=sandbox_exchanges,\n                display_ph_launch=display_ph_launch,\n                is_launching=is_launching,\n            )\n        else:\n            return flask.redirect(flask.url_for(\"terms\"))\n\n    @blueprint.route(\"/welcome\")\n    def welcome():\n        # used in terms page\n        return flask.redirect(flask.url_for(\"home\"))\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/dsl.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/dsl_help\", methods=[\"GET\"])\n    @login.login_required_when_activated\n    def dsl_help():\n        return flask.render_template('dsl_help.html')\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/errors.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.logging as bot_logging\nimport tentacles.Services.Interfaces.web_interface.util as util\n\nAPP_JSON_CONTENT_TYPE = \"application/json\"\n\n\ndef register(blueprint):\n    @blueprint.errorhandler(404)\n    def not_found(_):\n        if flask.request.content_type == APP_JSON_CONTENT_TYPE:\n            return util.get_rest_reply(\"We are sorry, but this doesn't exist\", 404)\n        return flask.render_template(\"404.html\"), 404\n\n    @blueprint.errorhandler(500)\n    def internal_error(error):\n        bot_logging.get_logger(\"WebInterfaceErrorHandler\").exception(error.original_exception, True,\n                                                                     f\"Error when displaying page: \"\n                                                                     f\"{error.original_exception}\")\n        if flask.request.content_type == APP_JSON_CONTENT_TYPE:\n            return util.get_rest_reply(f\"We are sorry, but an unexpected error occurred: {error.original_exception} \"\n                                       f\"({error.original_exception.__class__.__name__})\", 500)\n        return flask.render_template(\"500.html\",\n                                     error=error.original_exception), 500\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/home.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport time\nimport flask\n\nimport octobot_commons.authentication as authentication\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport tentacles.Services.Interfaces.web_interface.constants as web_constants\nimport octobot.constants as constants\nimport octobot_commons.constants\nimport octobot_commons.enums\n\n\ndef register(blueprint):\n    @blueprint.route(\"/\")\n    @blueprint.route(\"/home\")\n    @login.login_required_when_activated\n    def home():\n        if flask.request.args.get(\"reset_tutorials\", \"False\") == \"True\":\n            flask_util.BrowsingDataProvider.instance().set_first_displays(True)\n        if models.accepted_terms():\n            trading_delay_info = flask.request.args.get(\"trading_delay_info\", 'false').lower() == \"true\"\n            in_backtesting = models.get_in_backtesting_mode()\n            display_intro = flask_util.BrowsingDataProvider.instance().get_and_unset_is_first_display(\n                flask_util.BrowsingDataProvider.HOME\n            )\n            form_to_display = constants.WELCOME_FEEDBACK_FORM_ID\n            pnl_symbols = models.get_pnl_history_symbols()\n            all_time_frames = models.get_all_watched_time_frames()\n            display_time_frame = models.get_display_timeframe()\n            display_orders = models.get_display_orders()\n            sandbox_exchanges = models.get_sandbox_exchanges()\n            try:\n                user_id = models.get_user_account_id()\n                display_feedback_form = form_to_display and not models.has_filled_form(form_to_display)\n            except authentication.AuthenticationRequired:\n                # no authenticated user: don't display form\n                user_id = None\n                display_feedback_form = False\n            past_launch_time = (\n                web_constants.PRODUCT_HUNT_ANNOUNCEMENT_DAY\n                + (\n                        octobot_commons.enums.TimeFramesMinutes[octobot_commons.enums.TimeFrames.ONE_DAY]\n                        * octobot_commons.constants.MINUTE_TO_SECONDS\n                )\n            )\n            is_launching = (\n               web_constants.PRODUCT_HUNT_ANNOUNCEMENT_DAY\n               <= time.time()\n               <= past_launch_time\n            )\n\n            display_ph_launch = (\n                models.get_display_announcement(web_constants.PRODUCT_HUNT_ANNOUNCEMENT) or is_launching\n            ) and not time.time() > past_launch_time\n            return flask.render_template(\n                'index.html',\n                has_pnl_history=bool(pnl_symbols),\n                watched_symbols=models.get_watched_symbols(),\n                backtesting_mode=in_backtesting,\n                display_intro=display_intro,\n                display_trading_delay_info=trading_delay_info,\n                selected_profile=models.get_current_profile().name,\n                reference_unit=interfaces_util.get_reference_market(),\n                display_time_frame=display_time_frame,\n                display_orders=display_orders,\n                all_time_frames=all_time_frames,\n                user_id=user_id,\n                form_to_display=form_to_display,\n                display_feedback_form=display_feedback_form,\n                sandbox_exchanges=sandbox_exchanges,\n                display_ph_launch=display_ph_launch,\n                is_launching=is_launching,\n                latest_release_url=f\"{octobot_commons.constants.GITHUB_BASE_URL}/\"\n                                   f\"{octobot_commons.constants.GITHUB_ORGANISATION}/\"\n                                   f\"{constants.PROJECT_NAME}/releases/latest\",\n            )\n        else:\n            return flask.redirect(flask.url_for(\"terms\"))\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/interface_settings.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/watched_symbols\")\n    @blueprint.route('/watched_symbols', methods=['POST'])\n    @login.login_required_when_activated\n    def watched_symbols():\n        if flask.request.method == 'POST':\n            result = False\n            request_data = flask.request.get_json()\n            symbol = request_data[\"symbol\"]\n            action = request_data[\"action\"]\n            action_desc = \"added to\"\n            if action == 'add':\n                result = models.add_watched_symbol(symbol)\n            elif action == 'remove':\n                result = models.remove_watched_symbol(symbol)\n                action_desc = \"removed from\"\n            if result:\n                return util.get_rest_reply(flask.jsonify(f\"{symbol} {action_desc} watched markets\"))\n            else:\n                return util.get_rest_reply(f'Error: {symbol} not {action_desc} watched markets.', 500)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/logs.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport os\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.logging as logging\nimport octobot_tentacles_manager.constants as tentacles_manager_constants\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/logs\")\n    @login.login_required_when_activated\n    def logs():\n        web_interface.flush_errors_count()\n        return flask.render_template(\"logs.html\",\n                                     logs=web_interface.get_logs(),\n                                     notifications=web_interface.get_notifications_history())\n    \n    \n    @blueprint.route(\"/export_logs\")\n    @login.login_required_when_activated\n    def export_logs():\n        # use user folder as the bot always has the right to use it, on failure, try in tentacles folder\n        for candidate_path in (commons_constants.USER_FOLDER, tentacles_manager_constants.TENTACLES_PATH):\n            temp_file = os.path.abspath(os.path.join(os.getcwd(), candidate_path, \"exported_logs\"))\n            temp_file_with_ext = f\"{temp_file}.{models.LOG_EXPORT_FORMAT}\"\n            try:\n                if os.path.isdir(temp_file_with_ext):\n                    raise RuntimeError(f\"To be able to export logs, please remove or rename the {temp_file_with_ext} directory\")\n                elif os.path.isfile(temp_file_with_ext):\n                    os.remove(temp_file_with_ext)\n                file_path = models.export_logs(temp_file)\n                return flask_util.send_and_remove_file(file_path, \"logs_export.zip\")\n            except Exception as err:\n                logging.get_logger(\"export_logs\").exception(err, True, f\"Unexpected error when exporting logs: {err}\")\n                error = err\n        flask.flash(f\"Error when exporting logs: {error}.\", \"danger\")\n        return flask.redirect(flask.url_for(\"logs\"))\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/medias.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport os\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef _send_file(base_dir, file_path):\n    base_path, file_name = os.path.split(file_path)\n    return flask.send_from_directory(os.path.join(base_dir, base_path), file_name)\n\n\ndef register(blueprint):\n    @blueprint.route('/tentacle_media')\n    @blueprint.route('/tentacle_media/<path:path>')\n    @login.login_required_when_activated\n    def tentacle_media(path=None):\n        # images\n        if models.is_valid_tentacle_image_path(path):\n            # reference point is the web interface directory: use OctoBot root folder as a reference\n            return _send_file(\"../../../..\", path)\n    \n    \n    @blueprint.route('/profile_media/<path:path>')\n    @login.login_required_when_activated\n    def profile_media(path):\n        # images\n        if models.is_valid_profile_image_path(path):\n            # reference point is the web interface directory: use OctoBot root folder as a reference\n            return _send_file(\"../../../..\", path)\n    \n    \n    @blueprint.route('/exchange_logo/<name>')\n    @login.login_required_when_activated\n    def exchange_logo(name):\n        return flask.jsonify(models.get_exchange_logo(name))\n    \n    \n    @blueprint.route('/audio_media/<name>')\n    @login.login_required_when_activated\n    def audio_media(name):\n        if models.is_valid_audio_path(name):\n            # reference point is the web interface directory: use OctoBot root folder as a reference\n            return _send_file(\"static/audio\", name)\n    \n    \n    @blueprint.route('/currency_logos', methods=['POST'])\n    @login.login_required_when_activated\n    def cryptocurrency_logos():\n        request_data = flask.request.get_json()\n        return flask.jsonify(models.get_currency_logo_urls(request_data[\"currency_ids\"]))\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/octobot_authentication.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport flask_login\nimport flask_wtf\nimport wtforms\n\nimport octobot_commons.logging as bot_logging\nimport octobot_commons.authentication as authentication\nimport tentacles.Services.Interfaces.web_interface.login as web_login\nimport tentacles.Services.Interfaces.web_interface.security as security\n\nlogger = bot_logging.get_logger(\"ServerInstance Controller\")\n\n\ndef register(blueprint):\n    @blueprint.route('/login', methods=['GET', 'POST'])\n    def login():\n        # use default constructor to apply default values when no form in request\n        form = LoginForm(flask.request.form) if flask.request.form else LoginForm()\n        if form.validate_on_submit():\n            if blueprint.login_manager.is_valid_password(\n                    flask.request.remote_addr,\n                    form.password.data,\n                    form\n            ):\n                blueprint.login_manager.login_user(form.remember_me.data)\n                web_login.reset_attempts(flask.request.remote_addr)\n\n                return _get_next_url_or_home_redirect()\n            if web_login.register_attempt(flask.request.remote_addr):\n                if not form.password.errors:\n                    form.password.errors.append('Invalid password')\n                logger.warning(f\"Invalid login attempt from : {flask.request.remote_addr}\")\n            else:\n                form.password.errors.append('Too many attempts. Please restart your OctoBot to be able to login.')\n        return flask.render_template(\n            'login.html',\n            form=form,\n            is_remote_login=authentication.Authenticator.instance().must_be_authenticated_through_authenticator()\n        )\n\n\n    @blueprint.route(\"/logout\")\n    @flask_login.login_required\n    def logout():\n        flask_login.logout_user()\n        return _get_next_url_or_home_redirect()\n\n\n    def _get_next_url_or_home_redirect():\n        next_url = flask.request.args.get('next')\n        if not security.is_safe_url(next_url):\n            return flask.abort(400)\n        return flask.redirect(next_url or flask.url_for('home'))\n\n\nclass LoginForm(flask_wtf.FlaskForm):\n    password = wtforms.PasswordField('Password')\n    remember_me = wtforms.BooleanField('Remember me', default=True)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/octobot_help.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/octobot_help\")\n    @login.login_required_when_activated\n    def octobot_help():\n        return flask.render_template('octobot_help.html')\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/portfolio.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_commons.constants as commons_constants\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\"/portfolio\")\n    @login.login_required_when_activated\n    def portfolio():\n        has_real_trader, has_simulated_trader = interfaces_util.has_real_and_or_simulated_traders()\n\n        displayed_portfolio = models.get_exchange_holdings_per_symbol()\n        symbols_values = models.get_symbols_values(displayed_portfolio.keys(), has_real_trader, has_simulated_trader) \\\n            if displayed_portfolio else {}\n\n        _, _, portfolio_real_current_value, portfolio_simulated_current_value = \\\n            interfaces_util.get_portfolio_current_value()\n\n        displayed_portfolio_value = portfolio_real_current_value if has_real_trader else portfolio_simulated_current_value\n        reference_market = interfaces_util.get_reference_market()\n        initializing_currencies_prices_set = models.get_initializing_currencies_prices_set(\n            commons_constants.HOURS_TO_SECONDS\n        )\n\n        return flask.render_template('portfolio.html',\n                                     has_real_trader=has_real_trader,\n                                     has_simulated_trader=has_simulated_trader,\n                                     displayed_portfolio=displayed_portfolio,\n                                     symbols_values=symbols_values,\n                                     displayed_portfolio_value=round(displayed_portfolio_value, 8),\n                                     reference_unit=reference_market,\n                                     initializing_currencies_prices=initializing_currencies_prices_set,\n                                     )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/profiles.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.authentication as authentication\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.logging as commons_logging\nimport octobot_commons.enums as commons_enums\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.controllers.community_authentication as community_authentication\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_trading.api as trading_api\n\n\ndef register(blueprint):\n    @blueprint.route(\"/profiles_selector\")\n    @login.login_required_when_activated\n    def profiles_selector():\n        reboot = flask.request.args.get(\"reboot\", \"false\").lower() == \"true\"\n        onboarding = flask.request.args.get(\"onboarding\", 'false').lower() == \"true\"\n        use_cloud = flask.request.args.get(\"use_cloud\", 'false').lower() == \"true\"\n        models.wait_for_login_if_processing()\n\n        # skip profile selector when forced profile\n        if onboarding and models.get_forced_profile() is not None:\n            return flask.redirect(flask.url_for(\"trading_type_selector\", reboot=reboot, onboarding=onboarding))\n\n        profiles = models.get_profiles(commons_enums.ProfileType.LIVE)\n        current_profile = models.get_current_profile()\n        display_config = interfaces_util.get_edited_config()\n\n        config_exchanges = display_config[commons_constants.CONFIG_EXCHANGES]\n\n        enabled_exchanges = trading_api.get_enabled_exchanges_names(display_config)\n        media_url = flask.url_for(\"tentacle_media\", _external=True)\n        missing_tentacles = set()\n\n        logged_in_email = None\n        form = community_authentication.CommunityLoginForm(flask.request.form) \\\n            if flask.request.form else community_authentication.CommunityLoginForm()\n        authenticator = authentication.Authenticator.instance()\n        try:\n            logged_in_email = authenticator.get_logged_in_email()\n        except (authentication.AuthenticationRequired, authentication.UnavailableError, authentication.AuthenticationError):\n            pass\n        cloud_strategies = []\n        try:\n            cloud_strategies = models.get_cloud_strategies(authenticator)\n        except Exception as err:\n            # don't crash the page if this request fails\n            commons_logging.get_logger(\"profile_selector\").exception(\n                err, True, f\"Error when fetching cloud strategies: {err}\"\n            )\n        display_intro = flask_util.BrowsingDataProvider.instance().get_and_unset_is_first_display(\n            flask_util.BrowsingDataProvider.PROFILE_SELECTOR\n        )\n        return flask.render_template(\n            'profiles_selector.html',\n            show_nab_bar=not onboarding,\n            onboarding=onboarding,\n            read_only=True,\n            use_cloud=use_cloud,\n            reboot=reboot,\n            display_intro=display_intro,\n\n            current_logged_in_email=logged_in_email,\n            selected_user_bot=models.get_selected_user_bot(),\n            can_logout=models.can_logout(),\n            form=form,\n\n            current_profile=current_profile,\n            profiles=profiles.values(),\n            profiles_tentacles_details=models.get_profiles_tentacles_details(profiles),\n\n            cloud_strategies=cloud_strategies,\n\n            evaluator_config=models.get_evaluator_detailed_config(media_url, missing_tentacles),\n            strategy_config=models.get_strategy_config(media_url, missing_tentacles),\n\n            config_exchanges=config_exchanges,\n\n            symbol_list=sorted(models.get_symbol_list(enabled_exchanges or config_exchanges)),\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/reboot.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef register(blueprint):\n    @blueprint.route(\"/wait_reboot\")\n    @login.login_required_when_activated\n    def wait_reboot():\n        trading_delay_info = flask.request.args.get(\"trading_delay_info\", 'false').lower() == \"true\"\n        next_url = flask.request.args.get(\"next\", flask.url_for(\"home\", trading_delay_info=trading_delay_info))\n        reboot = flask.request.args.get(\"reboot\", \"false\").lower() == \"true\"\n        onboarding = flask.request.args.get(\"onboarding\", 'false').lower() == \"true\"\n\n        if reboot:\n            return_val = flask.render_template(\n                'wait_reboot.html',\n                show_nab_bar=not onboarding,\n                onboarding=onboarding,\n                next_url=next_url,\n                current_profile_name=models.get_current_profile().name,\n            )\n            if not models.is_rebooting():\n                reboot_delay = 2\n                # schedule reboot now that the page render has been computed\n                models.restart_bot(delay=reboot_delay)\n        else:\n            return_val = flask.redirect(next_url)\n        return return_val\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/robots.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\n\ndef register(blueprint):\n    @blueprint.route(\"/robots.txt\")\n    def robots():\n        return flask.render_template(\"robots.txt\")\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/tentacles_config.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot_commons.logging as commons_logging\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.util as util\n\n\ndef register(blueprint):\n    @blueprint.route('/config_tentacle', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def config_tentacle():\n        if flask.request.method == 'POST':\n            tentacle_name = flask.request.args.get(\"name\")\n            action = flask.request.args.get(\"action\")\n            profile_id = flask.request.args.get(\"profile_id\")\n            restart = flask.request.args.get(\"restart\", \"false\") == \"true\"\n            tentacles_setup_config = models.get_tentacles_setup_config_from_profile_id(profile_id) if profile_id else None\n            success = True\n            response = \"\"\n            reload_config = False\n            if action == \"update\":\n                request_data = flask.request.get_json()\n                success, response = models.update_tentacle_config(\n                    tentacle_name, request_data, tentacles_setup_config=tentacles_setup_config\n                )\n                reload_config = True\n            elif action == \"factory_reset\":\n                success, response = models.reset_config_to_default(\n                    tentacle_name, tentacles_setup_config=tentacles_setup_config\n                )\n                reload_config = True\n            if flask.request.args.get(\"reload\"):\n                try:\n                    models.reload_scripts()\n                except Exception as e:\n                    success = False\n                    response = str(e)\n            if reload_config and success:\n                try:\n                    models.reload_tentacle_config(tentacle_name)\n                except Exception as e:\n                    success = False\n                    response = f\"Error when reloading configuration {e}\"\n            if success:\n                if restart:\n                    models.schedule_delayed_command(models.restart_bot)\n                return util.get_rest_reply(flask.jsonify(response))\n            else:\n                return util.get_rest_reply(response, 500)\n        else:\n            if flask.request.args:\n                tentacle_name = flask.request.args.get(\"name\")\n                missing_tentacles = set()\n                media_url = flask.url_for(\"tentacle_media\", _external=True)\n                tentacle_class, tentacle_type, tentacle_desc = models.get_tentacle_from_string(tentacle_name, media_url)\n                is_strategy = tentacle_type == \"strategy\"\n                is_trading_mode = tentacle_type == \"trading mode\"\n                evaluator_config = None\n                strategy_config = None\n                requirements = tentacle_desc.get(models.REQUIREMENTS_KEY, [])\n                wildcard_requirements = requirements == [\"*\"]\n                if is_strategy and wildcard_requirements:\n                    evaluator_config = models.get_evaluator_detailed_config(\n                        media_url, missing_tentacles, single_strategy=tentacle_name\n                    )\n                elif is_trading_mode and len(requirements) > 1:\n                    strategy_config = models.get_strategy_config(\n                        media_url, missing_tentacles, with_trading_modes=False,\n                        whitelist=None if wildcard_requirements else requirements\n                    )\n                evaluator_startup_config = models.get_evaluators_tentacles_startup_activation() \\\n                    if evaluator_config or strategy_config else None\n                tentacle_commands = models.get_tentacle_user_commands(tentacle_class)\n                is_trading_strategy_configuration = models.is_trading_strategy_configuration(tentacle_type)\n                return flask.render_template(\n                    'config_tentacle.html',\n                    name=tentacle_name,\n                    tentacle_type=tentacle_type,\n                    tentacle_class=tentacle_class,\n                    tentacle_desc=tentacle_desc,\n                    evaluator_startup_config=evaluator_startup_config,\n                    strategy_config=strategy_config,\n                    evaluator_config=evaluator_config,\n                    is_trading_strategy_configuration=is_trading_strategy_configuration,\n                    activated_trading_mode=models.get_config_activated_trading_mode()\n                    if is_trading_strategy_configuration else None,\n                    data_files=models.get_data_files_with_description() if is_trading_strategy_configuration else None,\n                    missing_tentacles=missing_tentacles,\n                    user_commands=tentacle_commands,\n                    current_profile=models.get_current_profile()\n                )\n            else:\n                return flask.render_template('config_tentacle.html')\n\n\n    @blueprint.route('/config_tentacle_edit_details/<tentacle>')\n    @login.login_required_when_activated\n    def config_tentacle_edit_details(tentacle):\n        try:\n            profile_id = flask.request.args.get(\"profile\", None)\n            return util.get_rest_reply(\n                models.get_tentacle_config_and_edit_display(tentacle, profile_id=profile_id)\n            )\n        except Exception as e:\n            commons_logging.get_logger(\"configuration\").exception(e)\n            return util.get_rest_reply(str(e), 500)\n\n\n    @blueprint.route('/config_tentacles', methods=['POST'])\n    @login.login_required_when_activated\n    def config_tentacles():\n        action = flask.request.args.get(\"action\")\n        profile_id = flask.request.args.get(\"profile_id\")\n        tentacles_setup_config = models.get_tentacles_setup_config_from_profile_id(profile_id) if profile_id else None\n        success = True\n        response = \"\"\n        if action == \"update\":\n            request_data = flask.request.get_json()\n            responses = []\n            for tentacle, tentacle_config in request_data.items():\n                update_success, update_response = models.update_tentacle_config(\n                    tentacle, tentacle_config, tentacles_setup_config=tentacles_setup_config\n                )\n                success = update_success and success\n                responses.append(update_response)\n            response = \", \".join(responses)\n        if success and flask.request.args.get(\"reload\"):\n            try:\n                models.reload_activated_tentacles_config()\n            except Exception as e:\n                success = False\n                response = str(e)\n        if success:\n            return util.get_rest_reply(flask.jsonify(response))\n        else:\n            return util.get_rest_reply(response, 500)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/terms.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport octobot.disclaimer as disclaimer\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\n\n\ndef register(blueprint):\n    @blueprint.route(\"/terms\")\n    @login.login_required_when_activated\n    def terms():\n        return flask.render_template(\"terms.html\",\n                                     disclaimer=disclaimer.DISCLAIMER,\n                                     accepted_terms=models.accepted_terms())\n\n\n    @blueprint.route(\"/accept_terms\")\n    @login.login_required_when_activated\n    def accept_terms():\n        next_url = flask.request.args.get(\"next\", None)\n        if flask.request.args.get(\"accept_terms\", None) == \"True\":\n            models.accept_terms(True)\n            flask_util.BrowsingDataProvider.instance().set_first_displays(True)\n            return flask.redirect(next_url or flask.url_for(\"home\"))\n        return flask.redirect(flask.url_for(\"terms\"))\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/trading.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport datetime\nimport flask\n\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_commons.constants as commons_constants\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport octobot_trading.api as trading_api\n\n\ndef register(blueprint):\n    @blueprint.route(\"/symbol_market_status\")\n    @blueprint.route('/symbol_market_status', methods=['GET', 'POST'])\n    @login.login_required_when_activated\n    def symbol_market_status():\n        exchange_id = flask.request.args[\"exchange_id\"]\n        symbol = flask.request.args[\"symbol\"]\n        symbol_time_frames, exchange = models.get_exchange_watched_time_frames(exchange_id)\n        time_frames = list(symbol_time_frames)\n        time_frames.reverse()\n        symbol_evaluation = models.get_evaluation(symbol, exchange, exchange_id)\n        return flask.render_template('symbol_market_status.html',\n                                     symbol=symbol,\n                                     exchange=exchange,\n                                     exchange_id=exchange_id,\n                                     symbol_evaluation=symbol_evaluation,\n                                     time_frames=time_frames,\n                                     backtesting_mode=models.get_in_backtesting_mode())\n\n\n    @blueprint.route(\"/trading\")\n    @login.login_required_when_activated\n    def trading():\n        displayed_portfolio = models.get_exchange_holdings_per_symbol()\n        has_real_trader, has_simulated_trader = interfaces_util.has_real_and_or_simulated_traders()\n        symbols_values = models.get_symbols_values(displayed_portfolio.keys(), has_real_trader, has_simulated_trader) \\\n            if displayed_portfolio else {}\n        has_real_trader, _ = interfaces_util.has_real_and_or_simulated_traders()\n        exchanges_load = models.get_exchanges_load()\n        pnl_symbols = models.get_pnl_history_symbols()\n        return flask.render_template(\n            'trading.html',\n            might_have_positions=models.has_futures_exchange(),\n            watched_symbols=models.get_watched_symbols(),\n            pairs_with_status=interfaces_util.get_currencies_with_status(),\n            displayed_portfolio=displayed_portfolio,\n            symbols_values=symbols_values,\n            has_real_trader=has_real_trader,\n            exchanges_load=exchanges_load,\n            is_community_feed_connected=models.is_community_feed_connected(),\n            last_signal_time=models.get_last_signal_time(),\n            followed_strategy_url=models.get_followed_strategy_url(),\n            reference_market=interfaces_util.get_reference_market(),\n            pnl_symbols=pnl_symbols,\n            has_pnl_history=bool(pnl_symbols),\n        )\n\n\n    @blueprint.route(\"/trading_type_selector\")\n    @login.login_required_when_activated\n    def trading_type_selector():\n        onboarding = flask.request.args.get(\"onboarding\", 'false').lower() == \"true\"\n        display_config = interfaces_util.get_edited_config()\n\n        config_exchanges = display_config[commons_constants.CONFIG_EXCHANGES]\n        enabled_exchanges = trading_api.get_enabled_exchanges_names(display_config) or [models.get_default_exchange()]\n\n        current_profile = models.get_current_profile()\n\n        return_val = flask.render_template(\n            'trading_type_selector.html',\n            show_nab_bar=not onboarding,\n            onboarding=onboarding,\n\n            current_profile_name=current_profile.name,\n            config_exchanges=config_exchanges,\n            enabled_exchanges=enabled_exchanges,\n            exchanges_details=models.get_exchanges_details(config_exchanges),\n            ccxt_tested_exchanges=models.get_tested_exchange_list(),\n            ccxt_simulated_tested_exchanges=models.get_simulated_exchange_list(),\n            ccxt_other_exchanges=sorted(models.get_other_exchange_list()),\n\n            simulated_portfolio=models.get_json_simulated_portfolio(display_config),\n            portfolio_schema=models.JSON_PORTFOLIO_SCHEMA,\n            real_trader_activated=models.is_real_trading(current_profile),\n        )\n        return return_val\n\n\n    @blueprint.context_processor\n    def utility_processor():\n        def convert_timestamp(str_time):\n            return datetime.datetime.fromtimestamp(str_time).strftime('%Y-%m-%d %H:%M:%S')\n\n        def convert_type(order_type):\n            return order_type.name\n\n        return dict(convert_timestamp=convert_timestamp, convert_type=convert_type)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/controllers/welcome.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\n\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\ndef register(blueprint):\n    @blueprint.route(\"/welcome\")\n    @login.login_required_when_activated\n    def welcome():\n        return flask.render_template(\"welcome.html\")\n"
  },
  {
    "path": "Services/Interfaces/web_interface/enums.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport enum\n\n\nclass PriceStrings(enum.Enum):\n    STR_PRICE_TIME = \"time\"\n    STR_PRICE_CLOSE = \"close\"\n    STR_PRICE_OPEN = \"open\"\n    STR_PRICE_HIGH = \"high\"\n    STR_PRICE_LOW = \"low\"\n    STR_PRICE_VOL = \"vol\"\n\n\nclass TabsLocation(enum.Enum):\n    START = \"start\"\n    END = \"end\"\n\n\nclass ColorModes(enum.Enum):\n    LIGHT = \"light\"\n    DARK = \"dark\"\n    DEFAULT = \"light\"\n"
  },
  {
    "path": "Services/Interfaces/web_interface/errors.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nclass MissingExchangeId(Exception):\n    EXPLANATION = \"Invalid exchange id, this might be due to a recent restart. \" \\\n                  \"Refresh this page if the error persists\"\n    \"\"\"\n    Raised when an exchange id is not existing in the current bot\n    \"\"\"\n    pass\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import content_types_management\nfrom tentacles.Services.Interfaces.web_interface.flask_util.content_types_management import (\n    init_content_types,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import context_processor\nfrom tentacles.Services.Interfaces.web_interface.flask_util.context_processor import (\n    register_context_processor,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import file_services\nfrom tentacles.Services.Interfaces.web_interface.flask_util.file_services import (\n    send_and_remove_file,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import template_filters\nfrom tentacles.Services.Interfaces.web_interface.flask_util.template_filters import (\n    register_template_filters,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import json_provider\nfrom tentacles.Services.Interfaces.web_interface.flask_util.json_provider import (\n    FloatDecimalJSONProvider,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import cors\nfrom tentacles.Services.Interfaces.web_interface.flask_util.cors import (\n    get_user_defined_cors_allowed_origins,\n)\n\n\nfrom tentacles.Services.Interfaces.web_interface.flask_util import browsing_data_provider\nfrom tentacles.Services.Interfaces.web_interface.flask_util.browsing_data_provider import (\n    BrowsingDataProvider,\n)\n\n__all__ = [\n    \"init_content_types\",\n    \"register_context_processor\",\n    \"send_and_remove_file\",\n    \"register_template_filters\",\n    \"FloatDecimalJSONProvider\",\n    \"get_user_defined_cors_allowed_origins\",\n    \"BrowsingDataProvider\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/browsing_data_provider.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport os\nimport json\nimport secrets\nimport time\n\nimport octobot_commons.singleton as singleton\nimport octobot_commons.logging as logging\nimport octobot_commons.constants as constants\nimport octobot_commons.json_util as json_util\nimport octobot_commons.configuration as commons_configuration\nimport octobot_commons.authentication as commons_authentication\nfrom octobot_services.interfaces import run_in_bot_main_loop\nimport octobot.enums\n\n\n_PREFIX_BY_DISTRIBUTION = {\n    octobot.enums.OctoBotDistribution.DEFAULT.value: \"\",\n    octobot.enums.OctoBotDistribution.MARKET_MAKING.value: \"mm:\",\n}\n\n\nclass BrowsingDataProvider(singleton.Singleton):\n    SESSION_SEC_KEY = \"session_sec_key\"\n    FIRST_DISPLAY = \"first_display\"\n    CURRENCY_LOGO = \"currency_logo\"\n    ALL_CURRENCIES = \"all_currencies\"\n    HOME = \"home\"\n    PROFILE = \"profile\"\n    CONFIGURATION = \"configuration\"\n    AUTOMATIONS = \"automations\"\n    PROFILE_SELECTOR = \"profile_selector\"\n    CACHE_EXPIRATION = constants.DAYS_TO_SECONDS * 14   # use 14 days cache maximum\n    TIMESTAMP = \"timestamp\"\n    VALUE = \"value\"\n\n    def __init__(self):\n        self.browsing_data = {}\n        self.logger = logging.get_logger(self.__class__.__name__)\n        self._load_saved_data()\n\n    @staticmethod\n    def get_distribution_key(distribution: octobot.enums.OctoBotDistribution, key: str) -> str:\n        return f\"{_PREFIX_BY_DISTRIBUTION[distribution.value]}{key}\"\n\n    def get_or_create_session_secret_key(self):\n        try:\n            return self._get_session_secret_key()\n        except KeyError:\n            self._generate_session_secret_key()\n        except Exception as err:\n            self.logger.exception(err, True, f\"Unexpected error when reading session key: {err}\")\n            self._generate_session_secret_key()\n        return self._get_session_secret_key()\n\n    def get_and_unset_is_first_display(self, element):\n        try:\n            value = self.browsing_data[self.FIRST_DISPLAY][element]\n        except KeyError:\n            value = True\n        if value:\n            self.set_is_first_display(element, False)\n        return value\n\n    def set_is_first_display(self, element, is_first_display):\n        try:\n            if self.browsing_data[self.FIRST_DISPLAY][element] != is_first_display:\n                self.browsing_data[self.FIRST_DISPLAY][element] = is_first_display\n                self.dump_saved_data()\n        except KeyError:\n            self.browsing_data[self.FIRST_DISPLAY][element] = is_first_display\n            self.dump_saved_data()\n\n    def set_first_displays(self, is_first_display):\n        for key in self.browsing_data[self.FIRST_DISPLAY]:\n            self.browsing_data[self.FIRST_DISPLAY][key] = is_first_display\n        self.dump_saved_data()\n\n    def get_currency_logo_url(self, currency_id):\n        try:\n            return self.browsing_data[self.CURRENCY_LOGO][currency_id]\n        except KeyError:\n            return None\n\n    def set_currency_logo_url(self, currency_id, url, dump=True):\n        if url is None:\n            # do not save None as an url\n            return\n        self.browsing_data[self.CURRENCY_LOGO][currency_id] = url\n        if dump:\n            self.dump_saved_data()\n\n    def get_all_currencies(self):\n        return self._get_expiring_cached_value(self.ALL_CURRENCIES)\n\n    def set_all_currencies(self, all_currencies):\n        self._set_expiring_cached_value(self.ALL_CURRENCIES, all_currencies)\n        self.dump_saved_data()\n\n    def _get_session_secret_key(self):\n        authenticator = commons_authentication.Authenticator.instance()\n        if (\n            authenticator.must_be_authenticated_through_authenticator()\n            and not run_in_bot_main_loop(authenticator.has_login_info())\n        ):\n            # reset session key to force login\n            self.logger.debug(\"Regenerating session key as user is required but not connected.\")\n            self._generate_session_secret_key()\n        return commons_configuration.decrypt(self.browsing_data[self.SESSION_SEC_KEY]).encode()\n\n    def _create_session_secret_key(self):\n        # always generate a new unique session secret key, reuse it to save sessions after restart\n        # https://flask.palletsprojects.com/en/2.2.x/quickstart/#sessions\n        return commons_configuration.encrypt(secrets.token_hex()).decode()\n\n    def _generate_session_secret_key(self):\n        self.browsing_data[self.SESSION_SEC_KEY] = self._create_session_secret_key()\n        self.dump_saved_data()\n\n    def _get_default_data(self):\n        return {\n            self.SESSION_SEC_KEY: self._create_session_secret_key(),\n            self.FIRST_DISPLAY: {},\n            self.CURRENCY_LOGO: {},\n            self.ALL_CURRENCIES: self._create_expiring_cached_value([]),\n        }\n\n    def _apply_saved_data(self, read_data):\n        if not isinstance(read_data[self.ALL_CURRENCIES], dict):\n            # ensure previous version compatibility\n            read_data[self.ALL_CURRENCIES] = self._get_default_data()[self.ALL_CURRENCIES]\n        self.browsing_data.update(read_data)\n\n    def _load_saved_data(self):\n        self.browsing_data = self._get_default_data()\n        read_data = {}\n        try:\n            read_data = json_util.read_file(self._get_file())\n            self._apply_saved_data(read_data)\n        except FileNotFoundError:\n            pass\n        except Exception as err:\n            self.logger.exception(err, True, f\"Unexpected error when reading saved data: {err}\")\n        if any(key not in read_data for key in self.browsing_data):\n            # save fixed data\n            self.dump_saved_data()\n\n    def dump_saved_data(self):\n        try:\n            with open(self._get_file(), \"w\") as sessions_file:\n                return json.dump(self.browsing_data, sessions_file)\n        except Exception as err:\n            self.logger.exception(err, True, f\"Unexpected error when reading saved data: {err}\")\n\n    def _get_file(self):\n        return os.path.join(constants.USER_FOLDER, f\"{self.__class__.__name__}_data.json\")\n\n    def _get_expiring_cached_value(self, key):\n        self._ensure_cache_expiration(key)\n        return self.browsing_data[key][self.VALUE]\n\n    def _set_expiring_cached_value(self, key, value):\n        self.browsing_data[key] = self._create_expiring_cached_value(value)\n\n    def _create_expiring_cached_value(self, value):\n        return {\n            self.TIMESTAMP: time.time(),\n            self.VALUE: value,\n        }\n\n    def _ensure_cache_expiration(self, key):\n        if time.time() - self.browsing_data[key][self.TIMESTAMP] > self.CACHE_EXPIRATION:\n            self.browsing_data[key] = self._get_default_data()[key]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/content_types_management.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport mimetypes\n\n\ndef init_content_types():\n    # force mimetypes not to rely on system configuration\n    mimetypes.add_type('text/css', '.css')\n    mimetypes.add_type('application/javascript', '.js')\n    mimetypes.add_type('image/x-icon', '.ico')\n    mimetypes.add_type('image/svg+xml', '.svg')\n    mimetypes.add_type('font/woff2', '.woff2')\n    mimetypes.add_type('font/woff2', '.woff2')\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/context_processor.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.authentication as authentication\nimport octobot.constants as constants\nimport octobot.enums as enums\nimport octobot.community.identifiers_provider as identifiers_provider\nimport octobot.community.supabase_backend.enums as community_enums\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.models.configuration as configuration_model\nimport tentacles.Services.Interfaces.web_interface.enums as web_enums\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.constants as web_constants\nimport tentacles.Services.Interfaces.web_interface.login as web_interface_login\nimport octobot_services.interfaces as interfaces\nimport octobot_trading.util as trading_util\nimport octobot_trading.api as trading_api\nimport octobot_trading.enums as trading_enums\n\n\ndef register_context_processor(web_interface_instance):\n    @web_interface_instance.server_instance.context_processor\n    def context_processor_register():\n        def get_color_mode() -> str:\n            return models.get_color_mode().value\n\n        def get_distribution() -> str:\n            return models.get_distribution().value\n\n        def get_tentacle_config_file_content(tentacle_class):\n            return models.get_tentacle_config(tentacle_class)\n\n        def get_exchange_holdings(holdings, holding_type):\n            return ', '.join(f'{exchange.capitalize()}: {holding[holding_type]}'\n                             for exchange, holding in holdings['exchanges'].items())\n\n        def _get_details_from_full_symbol_list(full_symbol_list, currency_name):\n            for symbol_details in full_symbol_list:\n                if symbol_details[configuration_model.SHORT_NAME_KEY].lower() == currency_name:\n                    return symbol_details\n            raise KeyError(currency_name)\n\n        def get_currency_id(full_symbol_list, currency_name):\n            currency_key = currency_name.lower()\n            try:\n                return _get_details_from_full_symbol_list(full_symbol_list, currency_key)[configuration_model.ID_KEY]\n            except KeyError:\n                return currency_key\n\n        def filter_currency_pairs(currency, symbol_list_by_type, full_symbol_list, config_symbols):\n            currency_key = currency.lower()\n            try:\n                symbol = _get_details_from_full_symbol_list(full_symbol_list, currency_key)[configuration_model.SYMBOL_KEY]\n            except KeyError:\n                return symbol_list_by_type\n            filtered_symbol = {\n                symbol_type: [\n                    s\n                    for s in symbols\n                    if symbol in symbol_util.parse_symbol(s).base_and_quote()\n                ]\n                for symbol_type, symbols in symbol_list_by_type.items()\n            }\n            for symbol_type in list(filtered_symbol.keys()):\n                filtered_symbol[symbol_type] += [\n                    s\n                    for s in config_symbols[currency][commons_constants.CONFIG_CRYPTO_PAIRS]\n                    if s in symbol_list_by_type[symbol_type] and s not in filtered_symbol[symbol_type]\n                ]\n            return filtered_symbol\n\n        def get_profile_traded_pairs_by_currency(profile):\n            return {\n                currency: val[commons_constants.CONFIG_CRYPTO_PAIRS]\n                for currency, val in profile.config[commons_constants.CONFIG_CRYPTO_CURRENCIES].items()\n                if commons_constants.CONFIG_CRYPTO_PAIRS in val\n                    and val[commons_constants.CONFIG_CRYPTO_PAIRS]\n                    and trading_util.is_currency_enabled(profile.config, currency, True)\n            }\n\n        def get_profile_exchanges(profile):\n            return trading_api.get_enabled_exchanges_names(profile.config)\n\n        def is_supporting_future_trading(supported_exchange_types):\n            return trading_enums.ExchangeTypes.FUTURE in supported_exchange_types\n\n        def get_enabled_trader(profile):\n            if models.is_real_trading(profile):\n                return \"Real trading\"\n            if trading_util.is_trader_simulator_enabled(profile.config):\n                return \"Simulated trading\"\n            return \"\"\n\n        def get_filtered_list(origin_list, filtering_list):\n            return [\n                element\n                for element in origin_list\n                if element in filtering_list\n            ]\n\n        def get_plugin_tabs(location):\n            has_open_source_package = models.has_open_source_package()\n            for plugin in web_interface_instance.registered_plugins:\n                for tab in plugin.get_tabs():\n                    if tab.location is location and tab.is_available(has_open_source_package):\n                        yield tab\n\n        def is_in_stating_community_env():\n            return identifiers_provider.IdentifiersProvider.ENABLED_ENVIRONMENT is enums.CommunityEnvironments.Staging\n\n        def get_enabled_tentacles(tentacles_info_by_name):\n            for name, info in tentacles_info_by_name:\n                if info[web_constants.ACTIVATION_KEY]:\n                    return name\n\n        def get_logged_in_email():\n            try:\n                return authentication.Authenticator.instance().get_logged_in_email()\n            except (authentication.AuthenticationRequired, authentication.UnavailableError):\n                return \"\"\n\n        current_profile = models.get_current_profile()\n        trading_mode = models.get_config_activated_trading_mode()\n        selected_bot = models.get_selected_user_bot()\n        selected_bot_id = (selected_bot.get(community_enums.BotKeys.ID.value) or \"\") if selected_bot else \"\"\n\n\n        return dict(\n            LAST_UPDATED_STATIC_FILES=web_interface.LAST_UPDATED_STATIC_FILES,\n            OCTOBOT_WEBSITE_URL=constants.OCTOBOT_WEBSITE_URL,\n            OCTOBOT_DOCS_URL=constants.OCTOBOT_DOCS_URL,\n            DEVELOPER_DOCS_URL=constants.DEVELOPER_DOCS_URL,\n            EXCHANGES_DOCS_URL=constants.EXCHANGES_DOCS_URL,\n            OCTOBOT_FEEDBACK_URL=constants.OCTOBOT_FEEDBACK,\n            OCTOBOT_EXTENSION_PACKAGE_1_NAME=constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME,\n            OCTOBOT_COMMUNITY_URL=identifiers_provider.IdentifiersProvider.COMMUNITY_URL,\n            OCTOBOT_COMMUNITY_RECOVER_PASSWORD_URL=identifiers_provider.IdentifiersProvider.FRONTEND_PASSWORD_RECOVER_URL,\n            OCTOBOT_MARKET_MAKING_URL=constants.OCTOBOT_MARKET_MAKING_URL,\n            CURRENT_BOT_VERSION=interfaces.AbstractInterface.project_version,\n            LOCALE=constants.DEFAULT_LOCALE,\n            IS_DEMO=constants.IS_DEMO,\n            IS_CLOUD=constants.IS_CLOUD_ENV,\n            CAN_INSTALL_TENTACLES=constants.CAN_INSTALL_TENTACLES,\n            IS_ALLOWING_TRACKING=models.get_metrics_enabled(),\n            PH_TRACKING_ID=constants.PH_TRACKING_ID,\n            USER_EMAIL=get_logged_in_email(),\n            USER_SELECTED_BOT_ID=selected_bot_id,\n            PROFILE_NAME=current_profile.name,\n            TRADING_MODE_NAME=trading_mode.get_name() if trading_mode else \"\",\n            EXCHANGE_NAMES=\",\".join(get_profile_exchanges(current_profile)),\n            IS_REAL_TRADING=models.is_real_trading(current_profile),\n            TAB_START=web_enums.TabsLocation.START,\n            TAB_END=web_enums.TabsLocation.END,\n            get_color_mode=get_color_mode,\n            get_distribution=get_distribution,\n            get_tentacle_config_file_content=get_tentacle_config_file_content,\n            get_currency_id=get_currency_id,\n            filter_currency_pairs=filter_currency_pairs,\n            get_exchange_holdings=get_exchange_holdings,\n            get_profile_traded_pairs_by_currency=get_profile_traded_pairs_by_currency,\n            get_profile_exchanges=get_profile_exchanges,\n            get_enabled_trader=get_enabled_trader,\n            get_filtered_list=get_filtered_list,\n            get_current_profile=models.get_current_profile,\n            get_plugin_tabs=get_plugin_tabs,\n            get_enabled_tentacles=get_enabled_tentacles,\n            is_real_trading=models.is_real_trading,\n            is_supporting_future_trading=is_supporting_future_trading,\n            is_login_required=web_interface_login.is_login_required,\n            is_authenticated=web_interface_login.is_authenticated,\n            is_in_stating_community_env=is_in_stating_community_env,\n            startup_messages=models.get_startup_messages(),\n            are_automations_enabled=models.are_automations_enabled(),\n            is_backtesting_enabled=models.is_backtesting_enabled(),\n            is_advanced_interface_enabled=models.is_advanced_interface_enabled(),\n            has_open_source_package=models.has_open_source_package,\n            critical_notifications=web_interface.get_critical_notifications(),\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/cors.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport os\n\nimport octobot_services.constants as services_constants\n\n\ndef get_user_defined_cors_allowed_origins():\n    # Set services_constants.ENV_CORS_ALLOWED_ORIGINS env variable add stricter cors rules allowed origins\n    # example: http://localhost:5000\n    # Note: you can specify multiple origins using comma as a separator, ex: http://localhost:5000,https://a.com\n    cors_allowed_origins = os.getenv(services_constants.ENV_CORS_ALLOWED_ORIGINS, \"*\")\n    if \",\" in cors_allowed_origins:\n        return [origin.strip() for origin in cors_allowed_origins.split(\",\")]\n    return cors_allowed_origins\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/file_services.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport os\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef send_and_remove_file(file_path, download_name):\n    try:\n        return flask.send_file(file_path, as_attachment=True, download_name=download_name, max_age=0)\n    finally:\n        # cleanup temp_file\n        def remove_file(file_path):\n            try:\n                os.remove(file_path)\n            except Exception:\n                pass\n\n        models.schedule_delayed_command(remove_file, file_path, delay=10)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/json_provider.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport decimal\nimport flask.json.provider\n\n\nclass FloatDecimalJSONProvider(flask.json.provider.DefaultJSONProvider):\n\n    def dumps(self, obj, **kwargs):\n        if isinstance(obj, decimal.Decimal):\n            # Convert decimal instances to float.\n            return float(obj)\n        return super().dumps(obj, **kwargs)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/flask_util/template_filters.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\ndef register_template_filters(app):\n    # should only be called after app configuration\n\n    @app.template_filter()\n    def is_dict(value):\n        return isinstance(value, dict)\n\n    @app.template_filter()\n    def is_list(value):\n        return isinstance(value, list)\n\n    @app.template_filter()\n    def is_bool(value):\n        return isinstance(value, bool)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/login/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom tentacles.Services.Interfaces.web_interface.login import user\nfrom tentacles.Services.Interfaces.web_interface.login.user import (\n    User,\n)\n\n\nfrom tentacles.Services.Interfaces.web_interface.login import web_login_manager\nfrom tentacles.Services.Interfaces.web_interface.login.web_login_manager import (\n    WebLoginManager,\n    active_login_required,\n    login_required_when_activated,\n    register_attempt,\n    is_banned,\n    reset_attempts,\n    set_is_login_required,\n    is_login_required,\n    is_authenticated,\n    GENERIC_USER,\n)\n\n\n__all__ = [\n    \"User\",\n    \"WebLoginManager\",\n    \"active_login_required\",\n    \"login_required_when_activated\",\n    \"register_attempt\",\n    \"is_banned\",\n    \"reset_attempts\",\n    \"set_is_login_required\",\n    \"is_login_required\",\n    \"is_authenticated\",\n    \"GENERIC_USER\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/login/open_source_package_required.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport functools\n\nimport octobot.constants as constants\nimport tentacles.Services.Interfaces.web_interface.models as models\n\n\ndef open_source_package_required(func):\n    @functools.wraps(func)\n    def decorated_view(*args, **kwargs):\n        if models.has_open_source_package():\n            return func(*args, **kwargs)\n        flask.flash(f\"The {constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME} is required to use this page\")\n        return flask.redirect(flask.url_for('extensions'))\n    return decorated_view\n"
  },
  {
    "path": "Services/Interfaces/web_interface/login/user.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nclass User:\n    \"\"\"\n    Docs from https://flask-login.readthedocs.io/en/latest/#your-user-class\n    \"\"\"\n    GENERIC_USER_ID = \"user\"\n\n    def __init__(self):\n        # This property should return True if the user is authenticated, i.e. they have provided valid credentials.\n        # (Only authenticated users will fulfill the criteria of login_required.)\n        self.is_authenticated = False\n        # This property should return True if this is an active user - in addition to being authenticated, they also\n        # have activated their account, not been suspended, or any condition your application has for rejecting\n        # an account. Inactive accounts may not log in (without being forced of course).\n        self.is_active = True\n        # This property should return True if this is an anonymous user. (Actual users should return False instead.)\n        self.is_anonymous = False\n\n    def get_id(self):\n        \"\"\"\n        This method must return a unicode that uniquely identifies this user, and can be used to load the user\n        from the user_loader callback. Note that this must be a unicode - if the ID is natively an int or some other\n        type, you will need to convert it to unicode.\n        :return: The only octoBot user id\n        \"\"\"\n        return self.GENERIC_USER_ID\n"
  },
  {
    "path": "Services/Interfaces/web_interface/login/web_login_manager.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport functools\nimport flask_login\nimport flask\n\nimport octobot_commons.configuration as configuration\nimport octobot_commons.authentication as authentication\nimport octobot_commons.logging as logging\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot.constants as constants\nimport tentacles.Services.Interfaces.web_interface.login as login\n\nGENERIC_USER = login.User()\n_IS_LOGIN_REQUIRED = True\nIP_TO_CONNECTION_ATTEMPTS = {}\nMAX_CONNECTION_ATTEMPTS = 50\n\n\nclass WebLoginManager(flask_login.LoginManager):\n    def __init__(self, flask_app, password_hash):\n        # force is_authenticated to save login state throughout server restart\n        GENERIC_USER.is_authenticated = True\n        flask_login.LoginManager.__init__(self)\n        self.init_app(flask_app)\n        self.password_hash = password_hash\n        # register login view to redirect to when login is required\n        self.login_view = \"/login\"\n        self._register_callbacks()\n\n    def login_user(self, remember=False, duration=None, **kwargs):\n        # still set is_authenticated to be sure it's True on login\n        GENERIC_USER.is_authenticated = True\n        flask_login.login_user(GENERIC_USER, remember=remember, duration=duration, **kwargs)\n\n    def is_valid_password(self, ip, password, form):\n        authenticator = authentication.Authenticator.instance()\n        if authenticator.must_be_authenticated_through_authenticator():\n            try:\n                if constants.USER_ACCOUNT_EMAIL is None:\n                    raise authentication.AuthenticationError(\"Login impossible. \"\n                                                             \"USER_ACCOUNT_EMAIL constant must to be set\")\n                interfaces_util.run_in_bot_main_loop(\n                    authenticator.login(constants.USER_ACCOUNT_EMAIL, password),\n                    log_exceptions=False\n                )\n                return not is_banned(ip)\n            except authentication.FailedAuthentication:\n                return False\n            except Exception as e:\n                logging.get_logger(\"WebLoginManager\").exception(e, False)\n                form.password.errors.append(f\"Error during authentication: {e}\")\n                return False\n        return not is_banned(ip) and configuration.get_password_hash(password) == self.password_hash\n\n    def _register_callbacks(self):\n        @self.user_loader\n        def load_user(_):\n            # return None if user is invalid\n            return GENERIC_USER\n\n\ndef is_authenticated():\n    return flask_login.current_user.is_authenticated\n\n\ndef set_is_login_required(login_required):\n    global _IS_LOGIN_REQUIRED\n    _IS_LOGIN_REQUIRED = login_required\n\n\ndef is_login_required():\n    return _IS_LOGIN_REQUIRED or authentication.Authenticator.instance().must_be_authenticated_through_authenticator()\n\n\n@flask_login.login_required\ndef _login_required_func(func, *args, **kwargs):\n    return func(*args, **kwargs)\n\n\ndef login_required_when_activated(func):\n    @functools.wraps(func)\n    def decorated_view(*args, **kwargs):\n        if is_login_required():\n            return _login_required_func(func, *args, **kwargs)\n        return func(*args, **kwargs)\n    return decorated_view\n\n\ndef active_login_required(func):\n    @functools.wraps(func)\n    def decorated_view(*args, **kwargs):\n        if is_login_required():\n            return _login_required_func(func, *args, **kwargs)\n        flask.flash(f\"For security reasons, please enable password authentication in \"\n                    f\"accounts configuration to use the {flask.request.path} page.\",\n                    category=flask_login.LOGIN_MESSAGE_CATEGORY)\n        return flask.redirect('home')\n    return decorated_view\n\n\ndef register_attempt(ip):\n    if ip in IP_TO_CONNECTION_ATTEMPTS:\n        IP_TO_CONNECTION_ATTEMPTS[ip] += 1\n    else:\n        IP_TO_CONNECTION_ATTEMPTS[ip] = 1\n    return not is_banned(ip)\n\n\ndef is_banned(ip):\n    if ip in set(IP_TO_CONNECTION_ATTEMPTS.keys()):\n        return IP_TO_CONNECTION_ATTEMPTS[ip] >= MAX_CONNECTION_ATTEMPTS\n    return False\n\n\ndef reset_attempts(ip):\n    IP_TO_CONNECTION_ATTEMPTS[ip] = 0\n"
  },
  {
    "path": "Services/Interfaces/web_interface/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"WebInterface\"],\n  \"tentacles-requirements\": [\"web_service\"]\n}"
  },
  {
    "path": "Services/Interfaces/web_interface/models/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom tentacles.Services.Interfaces.web_interface.models import backtesting\nfrom tentacles.Services.Interfaces.web_interface.models import commands\nfrom tentacles.Services.Interfaces.web_interface.models import community\nfrom tentacles.Services.Interfaces.web_interface.models import configuration\nfrom tentacles.Services.Interfaces.web_interface.models import dashboard\nfrom tentacles.Services.Interfaces.web_interface.models import interface_settings\nfrom tentacles.Services.Interfaces.web_interface.models import logs\nfrom tentacles.Services.Interfaces.web_interface.models import medias\nfrom tentacles.Services.Interfaces.web_interface.models import profiles\nfrom tentacles.Services.Interfaces.web_interface.models import strategy_optimizer\nfrom tentacles.Services.Interfaces.web_interface.models import tentacles\nfrom tentacles.Services.Interfaces.web_interface.models import trading\nfrom tentacles.Services.Interfaces.web_interface.models import web_interface_tab\nfrom tentacles.Services.Interfaces.web_interface.models import json_schemas\nfrom tentacles.Services.Interfaces.web_interface.models import distributions\nfrom tentacles.Services.Interfaces.web_interface.models import dsl\n\nfrom tentacles.Services.Interfaces.web_interface.models.dsl import (\n    get_dsl_keywords_docs,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.models.backtesting import (\n    CURRENT_BOT_DATA,\n    get_full_candle_history_exchange_list,\n    get_other_history_exchange_list,\n    get_data_files_with_description,\n    start_backtesting_using_specific_files,\n    stop_previous_backtesting,\n    is_backtesting_enabled,\n    create_snapshot_data_collector,\n    get_data_files_from_current_bot,\n    start_backtesting_using_current_bot_data,\n    get_backtesting_status,\n    get_backtesting_report,\n    get_latest_backtesting_run_id,\n    get_delete_data_file,\n    get_data_collector_status,\n    stop_data_collector,\n    collect_data_file,\n    save_data_file,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.commands import (\n    schedule_delayed_command,\n    restart_bot,\n    is_rebooting,\n    stop_bot,\n    update_bot,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.community import (\n    get_community_metrics_to_display,\n    can_get_community_metrics,\n    get_owned_packages,\n    has_owned_packages_to_install,\n    has_open_source_package,\n    update_owned_packages,\n    get_checkout_url,\n    get_tradingview_email_address,\n    get_last_email_address_confirm_code_email_content,\n    wait_for_email_address_confirm_code_email,\n    get_cloud_strategies,\n    get_cloud_strategy,\n    get_preview_tentacles_packages,\n    get_current_octobots_stats,\n    get_all_user_bots,\n    get_selected_user_bot,\n    select_bot,\n    create_new_bot,\n    can_select_bot,\n    can_logout,\n    get_user_account_id,\n    has_filled_form,\n    register_user_submitted_form,\n    get_followed_strategy_url,\n    is_community_feed_connected,\n    get_last_signal_time,\n    sync_community_account,\n    wait_for_login_if_processing,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.json_schemas import (\n    NAME,\n    JSON_PORTFOLIO_SCHEMA,\n    JSON_TRADING_SIMULATOR_SCHEMA,\n    get_json_simulated_portfolio,\n    get_json_trading_simulator_config,\n    get_json_exchanges_schema,\n    get_json_exchange_config,\n    json_exchange_config_to_config,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.configuration import (\n    get_evaluators_tentacles_startup_activation,\n    get_trading_tentacles_startup_activation,\n    get_tentacle_documentation,\n    is_trading_strategy_configuration,\n    get_tentacle_from_string,\n    get_tentacle_user_commands,\n    are_automations_enabled,\n    is_advanced_interface_enabled,\n    restart_global_automations,\n    get_all_automation_steps,\n    has_at_least_one_running_automation,\n    get_automations_count,\n    reset_automation_config_to_default,\n    get_tentacle_config,\n    get_tentacle_config_and_user_inputs,\n    get_tentacle_config_and_edit_display,\n    get_tentacle_config_schema,\n    get_extra_tentacles_config_desc,\n    get_tentacles_activation_desc_by_group,\n    update_tentacle_config,\n    update_copied_trading_id,\n    reset_config_to_default,\n    get_strategy_config,\n    get_in_backtesting_mode,\n    accepted_terms,\n    accept_terms,\n    get_evaluator_detailed_config,\n    get_config_activated_trading_mode,\n    get_config_activated_strategies,\n    get_config_activated_evaluators,\n    has_futures_exchange,\n    update_tentacles_activation_config,\n    get_active_exchanges,\n    update_global_config,\n    activate_metrics,\n    activate_beta_env,\n    get_metrics_enabled,\n    get_beta_env_enabled_in_config,\n    get_services_list,\n    get_notifiers_list,\n    get_enabled_trading_pairs,\n    get_exchange_available_trading_pairs,\n    get_symbol_list,\n    get_all_currencies,\n    get_config_time_frames,\n    get_timeframes_list,\n    get_strategy_required_time_frames,\n    format_config_symbols,\n    format_config_symbols_without_enabled_key,\n    get_all_symbols_list,\n    get_all_symbols_list_by_symbol_type,\n    get_exchange_logo,\n    get_currency_logo_urls,\n    get_traded_time_frames,\n    get_full_exchange_list,\n    get_full_configurable_exchange_list,\n    get_default_exchange,\n    get_tested_exchange_list,\n    get_simulated_exchange_list,\n    get_other_exchange_list,\n    get_enabled_exchange_types,\n    get_exchanges_details,\n    are_compatible_accounts,\n    get_current_exchange,\n    REQUIREMENTS_KEY,\n    SYMBOL_KEY,\n    ID_KEY,\n    TRADING_MODES_KEY,\n    STRATEGIES_KEY,\n    change_reference_market_on_config_currencies,\n    send_command_to_activated_tentacles,\n    send_command_to_tentacles,\n    reload_scripts,\n    reload_activated_tentacles_config,\n    reload_tentacle_config,\n    update_config_currencies,\n    get_config_required_candles_count,\n    get_sandbox_exchanges,\n    get_distribution,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.dashboard import (\n    parse_get_symbol,\n    get_value_from_dict_or_string,\n    format_trades,\n    format_orders,\n    get_first_exchange_data,\n    get_watched_symbol_data,\n    get_startup_messages,\n    get_first_symbol_data,\n    get_currency_price_graph_update,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.interface_settings import (\n    add_watched_symbol,\n    remove_watched_symbol,\n    get_watched_symbols,\n    get_display_timeframe,\n    set_color_mode,\n    get_color_mode,\n    set_display_announcement,\n    get_display_announcement,\n    get_display_orders,\n    set_display_timeframe,\n    set_display_orders,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.logs import (\n    LOG_EXPORT_FORMAT,\n    export_logs,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.medias import (\n    is_valid_tentacle_image_path,\n    is_valid_profile_image_path,\n    is_valid_audio_path,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.profiles import (\n    get_current_profile,\n    get_profile,\n    get_tentacles_setup_config_from_profile_id,\n    get_tentacles_setup_config_from_profile,\n    duplicate_profile,\n    convert_to_live_profile,\n    select_profile,\n    get_profiles,\n    get_profiles_tentacles_details,\n    update_profile,\n    remove_profile,\n    export_profile,\n    import_profile,\n    download_and_import_profile,\n    import_strategy_as_profile,\n    get_profile_name,\n    get_forced_profile,\n    is_real_trading,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.strategy_optimizer import (\n    get_strategies_list,\n    get_time_frames_list,\n    get_evaluators_list,\n    get_risks_list,\n    cancel_optimizer,\n    start_optimizer,\n    get_optimizer_results,\n    get_optimizer_report,\n    get_current_run_params,\n    get_optimizer_status,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.tentacles import (\n    get_tentacles_packages,\n    get_official_tentacles_url,\n    call_tentacle_manager,\n    install_packages,\n    update_packages,\n    reset_packages,\n    update_modules,\n    uninstall_modules,\n    get_tentacles,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.trading import (\n    ensure_valid_exchange_id,\n    get_exchange_watched_time_frames,\n    get_all_watched_time_frames,\n    get_initializing_currencies_prices_set,\n    get_evaluation,\n    get_exchanges_load,\n    get_exchange_holdings_per_symbol,\n    get_symbols_values,\n    get_portfolio_historical_values,\n    get_pnl_history_symbols,\n    get_pnl_history,\n    get_all_orders_data,\n    get_all_trades_data,\n    get_all_positions_data,\n    clear_exchanges_orders_history,\n    clear_exchanges_trades_history,\n    clear_exchanges_transactions_history,\n    clear_exchanges_portfolio_history,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.web_interface_tab import (\n    WebInterfaceTab,\n)\nfrom tentacles.Services.Interfaces.web_interface.models.distributions import (\n    save_market_making_configuration,\n    get_market_making_services,\n)\n\n\n__all__ = [\n    \"get_data_files_with_description\",\n    \"start_backtesting_using_specific_files\",\n    \"stop_previous_backtesting\",\n    \"is_backtesting_enabled\",\n    \"create_snapshot_data_collector\",\n    \"get_data_files_from_current_bot\",\n    \"start_backtesting_using_current_bot_data\",\n    \"get_backtesting_status\",\n    \"get_backtesting_report\",\n    \"get_latest_backtesting_run_id\",\n    \"get_delete_data_file\",\n    \"get_data_collector_status\",\n    \"stop_data_collector\",\n    \"collect_data_file\",\n    \"save_data_file\",\n    \"schedule_delayed_command\",\n    \"restart_bot\",\n    \"is_rebooting\",\n    \"stop_bot\",\n    \"update_bot\",\n    \"get_community_metrics_to_display\",\n    \"can_get_community_metrics\",\n    \"get_owned_packages\",\n    \"has_owned_packages_to_install\",\n    \"has_open_source_package\",\n    \"update_owned_packages\",\n    \"get_checkout_url\",\n    \"get_tradingview_email_address\",\n    \"get_last_email_address_confirm_code_email_content\",\n    \"wait_for_email_address_confirm_code_email\",\n    \"get_cloud_strategies\",\n    \"get_cloud_strategy\",\n    \"get_preview_tentacles_packages\",\n    \"get_current_octobots_stats\",\n    \"get_all_user_bots\",\n    \"get_selected_user_bot\",\n    \"select_bot\",\n    \"create_new_bot\",\n    \"can_select_bot\",\n    \"can_logout\",\n    \"get_user_account_id\",\n    \"has_filled_form\",\n    \"register_user_submitted_form\",\n    \"get_followed_strategy_url\",\n    \"is_community_feed_connected\",\n    \"get_last_signal_time\",\n    \"sync_community_account\",\n    \"wait_for_login_if_processing\",\n    \"get_evaluators_tentacles_startup_activation\",\n    \"get_trading_tentacles_startup_activation\",\n    \"get_tentacle_documentation\",\n    \"is_trading_strategy_configuration\",\n    \"get_tentacle_from_string\",\n    \"get_tentacle_user_commands\",\n    \"are_automations_enabled\",\n    \"is_advanced_interface_enabled\",\n    \"restart_global_automations\",\n    \"get_all_automation_steps\",\n    \"has_at_least_one_running_automation\",\n    \"get_automations_count\",\n    \"reset_automation_config_to_default\",\n    \"get_tentacle_config\",\n    \"get_tentacle_config_and_user_inputs\",\n    \"get_tentacle_config_and_edit_display\",\n    \"get_tentacle_config_schema\",\n    \"get_extra_tentacles_config_desc\",\n    \"get_tentacles_activation_desc_by_group\",\n    \"update_tentacle_config\",\n    \"update_copied_trading_id\",\n    \"reset_config_to_default\",\n    \"get_strategy_config\",\n    \"get_in_backtesting_mode\",\n    \"accepted_terms\",\n    \"accept_terms\",\n    \"get_evaluator_detailed_config\",\n    \"get_config_activated_trading_mode\",\n    \"get_config_activated_strategies\",\n    \"get_config_activated_evaluators\",\n    \"has_futures_exchange\",\n    \"update_tentacles_activation_config\",\n    \"get_active_exchanges\",\n    \"update_global_config\",\n    \"activate_metrics\",\n    \"activate_beta_env\",\n    \"get_metrics_enabled\",\n    \"get_beta_env_enabled_in_config\",\n    \"get_services_list\",\n    \"get_notifiers_list\",\n    \"get_enabled_trading_pairs\",\n    \"get_exchange_available_trading_pairs\",\n    \"get_symbol_list\",\n    \"get_all_currencies\",\n    \"get_config_time_frames\",\n    \"get_timeframes_list\",\n    \"get_strategy_required_time_frames\",\n    \"format_config_symbols\",\n    \"format_config_symbols_without_enabled_key\",\n    \"get_all_symbols_list\",\n    \"get_exchange_logo\",\n    \"get_all_symbols_list_by_symbol_type\",\n    \"get_currency_logo_urls\",\n    \"get_json_simulated_portfolio\",\n    \"get_traded_time_frames\",\n    \"get_full_exchange_list\",\n    \"get_full_configurable_exchange_list\",\n    \"get_default_exchange\",\n    \"get_tested_exchange_list\",\n    \"get_simulated_exchange_list\",\n    \"get_other_exchange_list\",\n    \"get_exchanges_details\",\n    \"get_enabled_exchange_types\",\n    \"are_compatible_accounts\",\n    \"get_current_exchange\",\n    \"parse_get_symbol\",\n    \"get_value_from_dict_or_string\",\n    \"format_trades\",\n    \"format_orders\",\n    \"get_first_exchange_data\",\n    \"get_watched_symbol_data\",\n    \"get_first_symbol_data\",\n    \"get_currency_price_graph_update\",\n    \"get_watched_symbols\",\n    \"get_startup_messages\",\n    \"add_watched_symbol\",\n    \"remove_watched_symbol\",\n    \"get_display_timeframe\",\n    \"set_color_mode\",\n    \"get_color_mode\",\n    \"set_display_announcement\",\n    \"get_display_announcement\",\n    \"get_display_orders\",\n    \"set_display_timeframe\",\n    \"set_display_orders\",\n    \"LOG_EXPORT_FORMAT\",\n    \"export_logs\",\n    \"is_valid_tentacle_image_path\",\n    \"is_valid_profile_image_path\",\n    \"is_valid_audio_path\",\n    \"get_current_profile\",\n    \"get_profile\",\n    \"get_tentacles_setup_config_from_profile_id\",\n    \"get_tentacles_setup_config_from_profile\",\n    \"duplicate_profile\",\n    \"convert_to_live_profile\",\n    \"select_profile\",\n    \"get_profiles\",\n    \"get_profiles_tentacles_details\",\n    \"update_profile\",\n    \"remove_profile\",\n    \"export_profile\",\n    \"import_profile\",\n    \"download_and_import_profile\",\n    \"import_strategy_as_profile\",\n    \"get_profile_name\",\n    \"get_forced_profile\",\n    \"is_real_trading\",\n    \"get_strategies_list\",\n    \"get_time_frames_list\",\n    \"get_evaluators_list\",\n    \"get_risks_list\",\n    \"cancel_optimizer\",\n    \"start_optimizer\",\n    \"get_optimizer_results\",\n    \"get_optimizer_report\",\n    \"get_current_run_params\",\n    \"get_optimizer_status\",\n    \"get_tentacles_packages\",\n    \"get_official_tentacles_url\",\n    \"call_tentacle_manager\",\n    \"install_packages\",\n    \"update_packages\",\n    \"reset_packages\",\n    \"update_modules\",\n    \"uninstall_modules\",\n    \"get_tentacles\",\n    \"ensure_valid_exchange_id\",\n    \"get_exchange_watched_time_frames\",\n    \"get_all_watched_time_frames\",\n    \"get_initializing_currencies_prices_set\",\n    \"get_evaluation\",\n    \"get_exchanges_load\",\n    \"REQUIREMENTS_KEY\",\n    \"SYMBOL_KEY\",\n    \"ID_KEY\",\n    \"JSON_PORTFOLIO_SCHEMA\",\n    \"get_exchange_holdings_per_symbol\",\n    \"get_symbols_values\",\n    \"get_portfolio_historical_values\",\n    \"get_pnl_history_symbols\",\n    \"get_pnl_history\",\n    \"get_all_orders_data\",\n    \"get_all_trades_data\",\n    \"get_all_positions_data\",\n    \"clear_exchanges_orders_history\",\n    \"clear_exchanges_trades_history\",\n    \"clear_exchanges_transactions_history\",\n    \"clear_exchanges_portfolio_history\",\n    \"CURRENT_BOT_DATA\",\n    \"get_full_candle_history_exchange_list\",\n    \"get_other_history_exchange_list\",\n    \"change_reference_market_on_config_currencies\",\n    \"send_command_to_activated_tentacles\",\n    \"send_command_to_tentacles\",\n    \"reload_scripts\",\n    \"reload_activated_tentacles_config\",\n    \"reload_tentacle_config\",\n    \"update_config_currencies\",\n    \"get_config_required_candles_count\",\n    \"get_sandbox_exchanges\",\n    \"get_distribution\",\n    \"WebInterfaceTab\",\n    \"save_market_making_configuration\",\n    \"get_market_making_services\",\n    \"get_dsl_keywords_docs\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/backtesting.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport copy\nimport os\nimport asyncio\nimport ccxt\nimport threading\n\nimport octobot.strategy_optimizer\nimport octobot.api as octobot_api\nimport octobot.limits as octobot_limits\nimport octobot.constants as octobot_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.logging as bot_logging\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.databases as databases\nimport octobot_commons.constants as commons_constants\nimport octobot_backtesting.api as backtesting_api\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_backtesting.constants as backtesting_constants\nimport octobot_backtesting.enums as backtesting_enums\nimport octobot_backtesting.collectors as collectors\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_services.enums as services_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.api as trading_api\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Services.Interfaces.web_interface as web_interface_root\nimport tentacles.Services.Interfaces.web_interface.models.trading as trading_model\nimport tentacles.Services.Interfaces.web_interface.models.profiles as profiles_model\nimport tentacles.Services.Interfaces.web_interface.models.configuration as configuration_model\n\n\nSTOPPING_TIMEOUT = 30\nCURRENT_BOT_DATA = \"current_bot_data\"\n# data collector can be really slow, let it up to 2 hours to run\nDATA_COLLECTOR_TIMEOUT = 2 * commons_constants.HOURS_TO_SECONDS\n\n\ndef get_full_candle_history_exchange_list():\n    full_exchange_list = list(set(ccxt.exchanges))\n    return [exchange for exchange in trading_constants.FULL_CANDLE_HISTORY_EXCHANGES if exchange in full_exchange_list]\n\n\ndef get_other_history_exchange_list():\n    return [exchange for exchange in configuration_model.get_full_exchange_list() if\n            exchange not in trading_constants.FULL_CANDLE_HISTORY_EXCHANGES]\n\n\nasync def _get_description(data_file, files_with_description):\n    description = await backtesting_api.get_file_description(data_file)\n    if _is_usable_description(description):\n        files_with_description.append((data_file, description))\n\n\ndef _is_usable_description(description):\n    return description is not None \\\n           and description[backtesting_enums.DataFormatKeys.SYMBOLS.value] is not None \\\n           and description[backtesting_enums.DataFormatKeys.TIME_FRAMES.value] is not None\n\n\nasync def _retrieve_data_files_with_description(files):\n    files_with_description = []\n    await asyncio.gather(*[_get_description(data_file, files_with_description) for data_file in files])\n    return sorted(\n        files_with_description,\n        key=lambda f: f[1][backtesting_enums.DataFormatKeys.TIMESTAMP.value],\n        reverse=True\n    )\n\n\ndef get_data_files_with_description():\n    files = backtesting_api.get_all_available_data_files()\n    return interfaces_util.run_in_bot_async_executor(_retrieve_data_files_with_description(files))\n\n\ndef start_backtesting_using_specific_files(files, source, reset_tentacle_config=False, run_on_common_part_only=True,\n                                           start_timestamp=None, end_timestamp=None, trading_type=None,\n                                           enable_logs=False,\n                                           auto_stop=False, name=None, collector_start_callback=None, start_callback=None):\n    return _start_backtesting(files, source, reset_tentacle_config=reset_tentacle_config,\n                              run_on_common_part_only=run_on_common_part_only,\n                              start_timestamp=start_timestamp, end_timestamp=end_timestamp, trading_type=trading_type,\n                              use_current_bot_data=False, enable_logs=enable_logs,\n                              auto_stop=auto_stop, name=name, collector_start_callback=collector_start_callback,\n                              start_callback=start_callback)\n\n\ndef start_backtesting_using_current_bot_data(data_source, exchange_id, source, reset_tentacle_config=False,\n                                             start_timestamp=None, end_timestamp=None, trading_type=None,\n                                             profile_id=None,\n                                             enable_logs=False, auto_stop=False, name=None,\n                                             collector_start_callback=None, start_callback=None):\n    use_current_bot_data = data_source == CURRENT_BOT_DATA\n    files = None if use_current_bot_data else [data_source]\n    return _start_backtesting(files, source, reset_tentacle_config=reset_tentacle_config,\n                              run_on_common_part_only=False,\n                              start_timestamp=start_timestamp, end_timestamp=end_timestamp, trading_type=trading_type,\n                              profile_id=profile_id,\n                              use_current_bot_data=use_current_bot_data,\n                              exchange_id=exchange_id, enable_logs=enable_logs,\n                              auto_stop=auto_stop, name=name,\n                              collector_start_callback=collector_start_callback,\n                              start_callback=start_callback)\n\n\ndef stop_previous_backtesting():\n    previous_independent_backtesting = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_BACKTESTING]\n    if previous_independent_backtesting \\\n            and not octobot_api.is_independent_backtesting_stopped(previous_independent_backtesting):\n        interfaces_util.run_in_bot_main_loop(\n            octobot_api.stop_independent_backtesting(previous_independent_backtesting)\n        )\n        return True, \"Backtesting is stopping\"\n    return True, \"No backtesting to stop\"\n\n\ndef is_backtesting_enabled():\n    return octobot_constants.ENABLE_BACKTESTING\n\n\ndef _parse_trading_type(trading_type):\n    if trading_type is None or trading_type == commons_constants.USE_CURRENT_PROFILE:\n        return commons_constants.USE_CURRENT_PROFILE, commons_constants.USE_CURRENT_PROFILE\n    if trading_type == trading_enums.ExchangeTypes.SPOT.value:\n        return commons_constants.CONFIG_EXCHANGE_SPOT, commons_constants.USE_CURRENT_PROFILE\n    if trading_type == trading_enums.FutureContractType.INVERSE_PERPETUAL.value:\n        return commons_constants.CONFIG_EXCHANGE_FUTURE, trading_enums.FutureContractType.INVERSE_PERPETUAL\n    if trading_type == trading_enums.FutureContractType.LINEAR_PERPETUAL.value:\n        return commons_constants.CONFIG_EXCHANGE_FUTURE, trading_enums.FutureContractType.LINEAR_PERPETUAL\n    if trading_type == trading_enums.ExchangeTypes.MARGIN.value:\n        return commons_constants.CONFIG_EXCHANGE_MARGIN, commons_constants.USE_CURRENT_PROFILE\n    raise RuntimeError(f\"Unsupported trading type: {trading_type}\")\n\n\ndef _start_backtesting(files, source, reset_tentacle_config=False, run_on_common_part_only=True,\n                       start_timestamp=None, end_timestamp=None, trading_type=None, profile_id=None,\n                       use_current_bot_data=False,\n                       exchange_id=None, enable_logs=False, auto_stop=False,\n                       collector_start_callback=None, start_callback=None, name=None):\n    tools = web_interface_root.WebInterface.tools\n    if exchange_id is not None:\n        trading_model.ensure_valid_exchange_id(exchange_id)\n    try:\n        previous_independent_backtesting = tools[constants.BOT_TOOLS_BACKTESTING]\n        optimizer = tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n        is_optimizer_running = tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER] and \\\n                               interfaces_util.run_in_bot_async_executor(\n                                octobot_api.is_optimizer_in_progress(tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER])\n                               )\n        if is_optimizer_running and not isinstance(optimizer, octobot.strategy_optimizer.StrategyDesignOptimizer):\n            return False, \"An optimizer is already running\"\n        if use_current_bot_data and \\\n                isinstance(tools[constants.BOT_TOOLS_DATA_COLLECTOR], collectors.AbstractExchangeBotSnapshotCollector):\n            # can't start a new backtest with use_current_bot_data when a snapshot collector is on\n            return False, \"An data collector is already running\"\n        if tools[constants.BOT_PREPARING_BACKTESTING]:\n            return False, \"An backtesting is already running\"\n        if previous_independent_backtesting and \\\n                octobot_api.is_independent_backtesting_in_progress(previous_independent_backtesting):\n            return False, \"A backtesting is already running\"\n        else:\n            tools[constants.BOT_PREPARING_BACKTESTING] = True\n            if previous_independent_backtesting:\n                interfaces_util.run_in_bot_main_loop(\n                    octobot_api.stop_independent_backtesting(previous_independent_backtesting)\n                )\n            profile = profiles_model.get_current_profile()\n            if profile_id is not None:\n                profile = profiles_model.get_profile(profile_id)\n                config = profiles_model.get_profile(profile_id).config\n                tentacles_setup_config = profiles_model.get_tentacles_setup_config_from_profile_id(profile_id)\n            else:\n                if reset_tentacle_config:\n                    tentacles_config = interfaces_util.get_edited_config(dict_only=False).get_tentacles_config_path()\n                    tentacles_setup_config = tentacles_manager_api.get_tentacles_setup_config(tentacles_config)\n                else:\n                    tentacles_setup_config = interfaces_util.get_bot_api().get_edited_tentacles_config()\n                config = interfaces_util.get_edited_config()\n            # do not edit original config dict\n            config = copy.copy(config)\n            exchange_type, contract_type = _parse_trading_type(trading_type)\n            config[commons_constants.CONFIG_EXCHANGE_TYPE] = exchange_type\n            config[commons_constants.CONFIG_CONTRACT_TYPE] = contract_type\n            config[commons_constants.CONFIG_REQUIRED_EXTRA_TIMEFRAMES] = profile.extra_backtesting_time_frames\n            tools[constants.BOT_TOOLS_BACKTESTING_SOURCE] = source\n            if is_optimizer_running and files is None:\n                files = [get_data_files_from_current_bot(exchange_id, start_timestamp, end_timestamp,\n                                                         collect=False, profile_id=profile_id)]\n            if not is_optimizer_running and use_current_bot_data:\n                tools[constants.BOT_TOOLS_DATA_COLLECTOR] = \\\n                    create_snapshot_data_collector(exchange_id, start_timestamp, end_timestamp, profile_id=profile_id)\n                tools[constants.BOT_TOOLS_BACKTESTING] = None\n            else:\n                tools[constants.BOT_TOOLS_BACKTESTING] = octobot_api.create_independent_backtesting(\n                    config,\n                    tentacles_setup_config,\n                    files,\n                    run_on_common_part_only=run_on_common_part_only,\n                    start_timestamp=start_timestamp / 1000 if start_timestamp else None,\n                    end_timestamp=end_timestamp / 1000 if end_timestamp else None,\n                    enable_logs=enable_logs,\n                    stop_when_finished=auto_stop,\n                    name=name\n                )\n                tools[constants.BOT_TOOLS_DATA_COLLECTOR] = None\n            interfaces_util.run_in_bot_main_loop(\n                _collect_initialize_and_run_independent_backtesting(\n                    tools[constants.BOT_TOOLS_DATA_COLLECTOR], tools[constants.BOT_TOOLS_BACKTESTING],\n                    config, tentacles_setup_config, files, run_on_common_part_only,\n                    start_timestamp, end_timestamp, enable_logs, auto_stop, name,\n                    collector_start_callback, start_callback),\n                blocking=False,\n                timeout=DATA_COLLECTOR_TIMEOUT)\n            return True, \"Backtesting started\"\n    except Exception as e:\n        bot_logging.get_logger(\"DataCollectorWebInterfaceModel\").exception(e, False)\n        return False, f\"Error when starting backtesting: {e}\"\n    finally:\n        tools[constants.BOT_PREPARING_BACKTESTING] = False\n\n\nasync def _collect_initialize_and_run_independent_backtesting(\n        data_collector_instance, independent_backtesting, config, tentacles_setup_config, files, run_on_common_part_only,\n        start_timestamp, end_timestamp, enable_logs, auto_stop, name, collector_start_callback, start_callback):\n    logger = bot_logging.get_logger(\"StartIndependentBacktestingModel\")\n    if data_collector_instance is not None:\n        try:\n            if collector_start_callback:\n                collector_start_callback()\n            files = [await backtesting_api.initialize_and_run_data_collector(data_collector_instance)]\n        except Exception as e:\n            bot_logging.get_logger(\"DataCollectorModel\").exception(\n                e, True, f\"Error when collecting historical data: {e}\")\n            web_interface_root.WebInterface.tools[constants.BOT_PREPARING_BACKTESTING] = False\n            return\n        finally:\n            web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] = None\n            web_interface_root.WebInterface.tools[constants.BOT_TOOLS_BACKTESTING] = None\n    if independent_backtesting is None:\n        try:\n            if files is None:\n                raise RuntimeError(\"No datafiles\")\n            independent_backtesting = octobot_api.create_independent_backtesting(\n                config,\n                tentacles_setup_config,\n                files,\n                run_on_common_part_only=run_on_common_part_only,\n                start_timestamp=start_timestamp / 1000 if start_timestamp else None,\n                end_timestamp=end_timestamp / 1000 if end_timestamp else None,\n                enable_logs=enable_logs,\n                stop_when_finished=auto_stop,\n                name=name\n            )\n        except Exception as e:\n            logger.exception(e, True, f\"Error when initializing backtesting: {e}\")\n        finally:\n            # only unregister collector now that we can associate a backtesting\n            web_interface_root.WebInterface.tools[constants.BOT_TOOLS_BACKTESTING] = independent_backtesting\n            web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] = None\n    try:\n        web_interface_root.WebInterface.tools[constants.BOT_PREPARING_BACKTESTING] = False\n        if files is not None:\n            if start_callback:\n                start_callback()\n            await octobot_api.initialize_and_run_independent_backtesting(independent_backtesting, log_errors=False)\n        else:\n            logger.error(f\"Data files is None when initializing backtesting: impossible to start\")\n    except Exception as e:\n        message = f\"Error when running backtesting: {e}\"\n        logger.exception(e, True, message)\n        await web_interface_root.add_notification(services_enums.NotificationLevel.ERROR, \"Backtesting\", message)\n        try:\n            await octobot_api.stop_independent_backtesting(independent_backtesting)\n            web_interface_root.WebInterface.tools[constants.BOT_TOOLS_BACKTESTING] = None\n        except Exception as e:\n            logger.exception(e, True, f\"Error when stopping backtesting: {e}\")\n\n\ndef get_backtesting_status():\n    if web_interface_root.WebInterface.tools[constants.BOT_TOOLS_BACKTESTING] is not None:\n        independent_backtesting = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_BACKTESTING]\n        if octobot_api.is_independent_backtesting_in_progress(independent_backtesting):\n            return \"computing\", octobot_api.get_independent_backtesting_progress(independent_backtesting) * 100, \\\n                bot_logging.get_backtesting_errors_count()\n        if octobot_api.is_independent_backtesting_finished(independent_backtesting) or \\\n                octobot_api.is_independent_backtesting_stopped(independent_backtesting):\n            return \"finished\", 100, bot_logging.get_backtesting_errors_count()\n        return \"starting\", 0, 0\n    return \"not started\", 0, 0\n\n\ndef get_backtesting_report(source):\n    tools = web_interface_root.WebInterface.tools\n    if tools[constants.BOT_TOOLS_BACKTESTING]:\n        independent_backtesting = tools[constants.BOT_TOOLS_BACKTESTING]\n        if tools[constants.BOT_TOOLS_BACKTESTING_SOURCE] == source:\n            return {\n                \"report\": interfaces_util.run_in_bot_async_executor(\n                    octobot_api.get_independent_backtesting_report(independent_backtesting)\n                ),\n                \"trades\": trading_model.get_all_trades_data(independent_backtesting=independent_backtesting)\n            }\n    return {}\n\n\ndef get_latest_backtesting_run_id(trading_mode):\n    tools = web_interface_root.WebInterface.tools\n    if tools[constants.BOT_TOOLS_BACKTESTING]:\n        backtesting = tools[constants.BOT_TOOLS_BACKTESTING]\n        interfaces_util.run_in_bot_main_loop(octobot_api.join_independent_backtesting_stop(backtesting,\n                                                                                           STOPPING_TIMEOUT))\n        bot_id = octobot_api.get_independent_backtesting_bot_id(backtesting)\n        return {\n            \"id\": databases.RunDatabasesProvider.instance().get_run_databases_identifier(\n                bot_id\n            ).backtesting_id\n        }\n    return {}\n\n\ndef get_delete_data_file(file_name):\n    deleted, error = backtesting_api.delete_data_file(file_name)\n    if deleted:\n        return deleted, f\"{file_name} deleted\"\n    else:\n        return deleted, f\"Can't delete {file_name} ({error})\"\n\n\ndef get_data_collector_status():\n    progress = {\"current_step\": 0, \"total_steps\": 0, \"current_step_percent\": 0}\n    if web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] is not None:\n        data_collector = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR]\n        if backtesting_api.is_data_collector_in_progress(data_collector):\n            current_step, total_steps, current_step_percent = \\\n                backtesting_api.get_data_collector_progress(data_collector)\n            progress[\"current_step\"] = current_step\n            progress[\"total_steps\"] = total_steps\n            progress[\"current_step_percent\"] = current_step_percent\n            return \"collecting\", progress\n        if backtesting_api.is_data_collector_finished(data_collector):\n            return \"finished\", progress\n        return \"starting\", progress\n    return \"not started\", progress\n\n\ndef stop_data_collector():\n    success = False\n    message = \"Failed to stop data collector\"\n    if web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] is not None:\n        success = interfaces_util.run_in_bot_main_loop(backtesting_api.stop_data_collector(web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR]))\n        message = \"Data collector stopped\"\n        web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] = None\n    return success, message\n\n\ndef create_snapshot_data_collector(exchange_id, start_timestamp, end_timestamp, profile_id=None):\n    exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n    symbols = trading_api.get_trading_symbols(exchange_manager)\n    time_frames = trading_api.get_relevant_time_frames(exchange_manager)\n    if profile_id is not None:\n        profile = profiles_model.get_profile(profile_id)\n        tentacles_setup_config = profiles_model.get_tentacles_setup_config_from_profile(profile)\n        strategies = configuration_model.get_config_activated_strategies(tentacles_setup_config)\n        time_frames = list(set(\n            [\n                tf\n                for tf in configuration_model.get_traded_time_frames(\n                    exchange_manager, strategies=strategies, tentacles_setup_config=tentacles_setup_config\n                ) or (commons_enums.TimeFrames.ONE_MINUTE,)\n            ] + [\n                commons_enums.TimeFrames(tf)\n                for tf in profile.extra_backtesting_time_frames\n            ]\n        ))\n        exchange_symbols = trading_api.get_all_exchange_symbols(exchange_manager)\n        profile_symbols = trading_api.get_config_symbols(profile.config, True)\n        symbols = [\n            commons_symbols.parse_symbol(symbol)\n            for symbol in profile_symbols\n            if symbol in exchange_symbols\n        ]\n        if len(symbols) < len(profile_symbols):\n            skipped = [\n                symbol\n                for symbol in profile_symbols\n                if commons_symbols.parse_symbol(symbol) not in symbols\n            ]\n            bot_logging.get_logger(\"DataCollectorWebInterfaceModel\").error(\n                f\"Skipping {skipped} symbol(s) for backtesting as they \"\n                f\"are not available on {trading_api.get_exchange_name(exchange_manager)}\"\n            )\n\n    return backtesting_api.exchange_bot_snapshot_data_collector_factory(\n        trading_api.get_exchange_name(exchange_manager),\n        interfaces_util.get_bot_api().get_edited_tentacles_config(),\n        symbols,\n        exchange_id,\n        time_frames=time_frames,\n        start_timestamp=start_timestamp,\n        end_timestamp=end_timestamp,\n        config=interfaces_util.get_bot_api().get_edited_config(dict_only=True),\n    )\n\n\ndef get_data_files_from_current_bot(exchange_id, start_timestamp, end_timestamp, collect=True, profile_id=None):\n    data_collector_instance = create_snapshot_data_collector(exchange_id, start_timestamp, end_timestamp,\n                                                             profile_id=profile_id)\n    if not collect:\n        return data_collector_instance.file_name\n    web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] = data_collector_instance\n    try:\n        collected_files = interfaces_util.run_in_bot_main_loop(\n            backtesting_api.initialize_and_run_data_collector(data_collector_instance),\n            timeout=DATA_COLLECTOR_TIMEOUT\n        )\n        return collected_files\n    finally:\n        web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] = None\n\n\ndef collect_data_file(exchange, symbols, time_frames=None, start_timestamp=None, end_timestamp=None):\n    if not is_backtesting_enabled():\n        return False, \"Backtesting is disabled.\"\n    if not exchange:\n        return False, \"Please select an exchange.\"\n    if not symbols:\n        return False, \"Please select a trading pair.\"\n    if message := _ensure_backtesting_limits(\n        exchange, symbols, time_frames, start_timestamp, end_timestamp, \"collect data\"\n    ):\n        return False, message\n    if web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] is None or \\\n            backtesting_api.is_data_collector_finished(\n                web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR]):\n        if time_frames is not None:\n            time_frames = time_frames if isinstance(time_frames, list) else [time_frames]\n            if not any(isinstance(time_frame, commons_enums.TimeFrames) for time_frame in time_frames):\n                time_frames = time_frame_manager.parse_time_frames(time_frames)\n        first_symbol = commons_symbols.parse_symbol(symbols[0])\n        exchange_type = trading_enums.ExchangeTypes.SPOT if first_symbol.is_spot() \\\n            else trading_enums.ExchangeTypes.FUTURE if first_symbol.is_future() \\\n            else trading_enums.ExchangeTypes.UNKNOWN\n        _background_collect_exchange_historical_data(exchange, exchange_type, symbols, time_frames,\n                                                     start_timestamp, end_timestamp)\n        return True, f\"Historical data collection started.\"\n    else:\n        return False, f\"Can't collect data for {symbols} on {exchange} (Historical data collector is already running)\"\n\n\nasync def _start_collect_and_notify(data_collector_instance):\n    success = False\n    message = \"finished\"\n    try:\n        await backtesting_api.initialize_and_run_data_collector(data_collector_instance)\n        success = True\n    except Exception as e:\n        message = f\"error: {e}\"\n    notification_level = services_enums.NotificationLevel.SUCCESS if success else services_enums.NotificationLevel.ERROR\n    await web_interface_root.add_notification(notification_level, f\"Data collection\", message)\n\n\ndef _background_collect_exchange_historical_data(exchange, exchange_type, symbols, time_frames,\n                                                 start_timestamp, end_timestamp):\n    data_collector_instance = backtesting_api.exchange_historical_data_collector_factory(\n        exchange,\n        exchange_type,\n        interfaces_util.get_bot_api().get_edited_tentacles_config(),\n        [commons_symbols.parse_symbol(symbol) for symbol in symbols],\n        time_frames=time_frames,\n        start_timestamp=start_timestamp,\n        end_timestamp=end_timestamp,\n        config=interfaces_util.get_bot_api().get_edited_config(dict_only=True),\n    )\n    web_interface_root.WebInterface.tools[constants.BOT_TOOLS_DATA_COLLECTOR] = data_collector_instance\n    coro = _start_collect_and_notify(data_collector_instance)\n    threading.Thread(target=asyncio.run, args=(coro,), name=f\"DataCollector{symbols}\").start()\n\n\nasync def _convert_into_octobot_data_file_if_necessary(output_file):\n    try:\n        description = await backtesting_api.get_file_description(output_file, data_path=\"\")\n        if description is not None:\n            # no error: current bot format data\n            return f\"{output_file} saved\"\n        else:\n            # try to convert into current bot format\n            converted_output_file = await backtesting_api.convert_data_file(output_file)\n            if converted_output_file is not None:\n                message = f\"Saved into {converted_output_file}\"\n            else:\n                message = \"Failed to convert file.\"\n            # remove invalid format file\n            os.remove(output_file)\n            return message\n    except Exception as e:\n        message = f\"Error when handling backtesting data file: {e}\"\n        bot_logging.get_logger(\"DataCollectorWebInterfaceModel\").exception(e, True, message)\n        return message\n\n\ndef save_data_file(name, file):\n    try:\n        output_file = f\"{backtesting_constants.BACKTESTING_FILE_PATH}/{name}\"\n        file.save(output_file)\n        message = interfaces_util.run_in_bot_async_executor(_convert_into_octobot_data_file_if_necessary(output_file))\n        bot_logging.get_logger(\"DataCollectorWebInterfaceModel\").info(message)\n        return True, message\n    except Exception as e:\n        message = f\"Error when saving file: {e}. File can't be saved.\"\n        bot_logging.get_logger(\"DataCollectorWebInterfaceModel\").error(message)\n        return False, message\n\n\ndef _ensure_backtesting_limits(exchange, symbols, time_frames, start_timestamp, end_timestamp, action):\n    message = \"\"\n    try:\n        start_timestamp = start_timestamp / commons_constants.MSECONDS_TO_SECONDS if start_timestamp else start_timestamp\n        end_timestamp = end_timestamp / commons_constants.MSECONDS_TO_SECONDS if end_timestamp else end_timestamp\n        octobot_limits.ensure_backtesting_limits([exchange], symbols, time_frames, start_timestamp, end_timestamp)\n    except octobot_limits.ReachedLimitError as err:\n        message = f\"Can't {action} for {symbols} on {exchange}: {err}\"\n        bot_logging.get_logger(\"BacktestingModel\").error(message)\n    return message\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/commands.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport threading\nimport time\n\nimport octobot_services.interfaces.util as interfaces_util\n\n_PENDING_COMMANDS = set()\n_REBOOT = \"reboot\"\n\n\ndef schedule_delayed_command(command, *args, delay=0.5):\n    def _delayed_command():\n        time.sleep(delay)\n        command(*args)\n    threading.Thread(target=_delayed_command).start()\n\n\ndef restart_bot(delay=None):\n    _PENDING_COMMANDS.add(_REBOOT)\n    if delay:\n        # recall self with delay\n        schedule_delayed_command(restart_bot, delay=delay)\n        return\n    _PENDING_COMMANDS.remove(_REBOOT)\n    interfaces_util.get_bot_api().restart_bot()\n\n\ndef is_rebooting():\n    return _REBOOT in _PENDING_COMMANDS\n\n\ndef stop_bot():\n    interfaces_util.get_bot_api().stop_bot()\n\n\ndef update_bot():\n    interfaces_util.get_bot_api().update_bot()\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/community.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport typing\n\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot.community as octobot_community\nimport octobot.commands as octobot_commands\nimport octobot.constants as octobot_constants\nimport octobot_commons.authentication as authentication\nimport octobot_trading.api as trading_api\n\n\ndef get_community_metrics_to_display():\n    return interfaces_util.run_in_bot_async_executor(octobot_community.get_community_metrics())\n\n\ndef can_get_community_metrics():\n    return octobot_community.can_read_metrics(interfaces_util.get_edited_config(dict_only=False))\n\n\ndef get_owned_packages() -> list[str]:\n    authenticator = authentication.Authenticator.instance()\n    return authenticator.get_owned_packages()\n\n\ndef has_owned_packages_to_install() -> list[str]:\n    authenticator = authentication.Authenticator.instance()\n    return authenticator.has_owned_packages_to_install()\n\n\ndef update_owned_packages():\n    authenticator = authentication.Authenticator.instance()\n    interfaces_util.run_in_bot_main_loop(authenticator.fetch_private_data(reset=True))\n\n\ndef has_open_source_package() -> bool:\n    authenticator = authentication.Authenticator.instance()\n    return authenticator.has_open_source_package()\n\n\ndef get_checkout_url(payment_method, redirect_url) -> (bool, str):\n    selected_payment_method = \"crypto\" if payment_method == \"crypto\" else \"credit_card\"\n    authenticator = authentication.Authenticator.instance()\n    try:\n        url = interfaces_util.run_in_bot_main_loop(authenticator.fetch_checkout_url(selected_payment_method, redirect_url))\n        return True, url\n    except BaseException:\n        return False, \"error when fetching checkout url\"\n\n\ndef get_tradingview_email_address() -> str:\n    return authentication.Authenticator.instance().get_saved_tradingview_email()\n\n\ndef get_last_email_address_confirm_code_email_content() -> typing.Optional[str]:\n    return authentication.Authenticator.instance().get_last_email_address_confirm_code_email_content()\n\n\ndef wait_for_email_address_confirm_code_email():\n    return interfaces_util.run_in_bot_main_loop(\n        authentication.Authenticator.instance().trigger_wait_for_email_address_confirm_code_email()\n    )\n\n\ndef get_cloud_strategies(authenticator) -> list[octobot_community.StrategyData]:\n    return interfaces_util.run_in_bot_main_loop(authenticator.get_strategies())\n\n\ndef get_cloud_strategy(authenticator, strategy_id: str) -> octobot_community.StrategyData:\n    return interfaces_util.run_in_bot_main_loop(authenticator.get_strategy(strategy_id))\n\n\ndef get_preview_tentacles_packages(url_for):\n    c1 = octobot_community.CommunityTentaclesPackage(\n        \"AI candles analyser\",\n        \"Tentacles packages offering artificial intelligence analysis tools based on candles shapes.\",\n        None, True,\n        [url_for(\"static\", filename=\"img/community/tentacles_packages_previews/octobot.png\")], None, None, None)\n    c1.uninstalled = False\n    c2 = octobot_community.CommunityTentaclesPackage(\n        \"Telegram portfolio management\",\n        \"Manage your portfolio directly from the telegram interface.\",\n        None, False,\n        [url_for(\"static\", filename=\"img/community/tentacles_packages_previews/telegram.png\")], None, None, None)\n    c2.uninstalled = False\n    c3 = octobot_community.CommunityTentaclesPackage(\n        \"Mobile first web interface\",\n        \"Use a mobile oriented interface for your OctoBot.\",\n        None, True,\n        [url_for(\"static\", filename=\"img/community/tentacles_packages_previews/mobile.png\")], None, None, None)\n    c3.uninstalled = True\n    return [c1, c2, c3]\n\n\ndef get_current_octobots_stats():\n    return interfaces_util.run_in_bot_async_executor(octobot_community.get_current_octobots_stats())\n\n\ndef _format_bot(bot):\n    return {\n        \"name\": octobot_community.CommunityUserAccount.get_bot_name_or_id(bot) if bot else None,\n        \"id\": octobot_community.CommunityUserAccount.get_bot_id(bot) if bot else None,\n    }\n\n\ndef get_all_user_bots():\n    # reload user bots to make sure the list is up to date\n    interfaces_util.run_in_bot_main_loop(authentication.Authenticator.instance().load_user_bots())\n    return sorted([\n        _format_bot(bot)\n        for bot in authentication.Authenticator.instance().user_account.get_all_user_bots_raw_data()\n    ], key=lambda d: d[\"name\"])\n\n\ndef get_selected_user_bot():\n    return _format_bot(authentication.Authenticator.instance().user_account.get_selected_bot_raw_data())\n\n\ndef select_bot(bot_id):\n    interfaces_util.run_in_bot_main_loop(authentication.Authenticator.instance().select_bot(bot_id))\n\n\ndef create_new_bot():\n    return interfaces_util.run_in_bot_main_loop(authentication.Authenticator.instance().create_new_bot())\n\n\ndef can_select_bot():\n    return not octobot_constants.COMMUNITY_BOT_ID\n\n\ndef can_logout():\n    return not authentication.Authenticator.instance().must_be_authenticated_through_authenticator()\n\n\ndef get_user_account_id():\n    return authentication.Authenticator.instance().get_user_id()\n\n\ndef has_filled_form(form_id):\n    return authentication.Authenticator.instance().has_filled_form(form_id)\n\n\ndef register_user_submitted_form(user_id, form_id):\n    try:\n        if get_user_account_id() != user_id:\n            return False, \"Invalid user id\"\n        interfaces_util.run_in_bot_main_loop(\n            authentication.Authenticator.instance().register_filled_form(form_id)\n        )\n    except Exception as e:\n        return False, f\"Error when registering filled form {e}\"\n    return True, \"Thank you for your feedback !\"\n\n\ndef get_followed_strategy_url():\n    trading_mode = interfaces_util.get_bot_api().get_trading_mode()\n    if trading_mode is None:\n        return None\n    identifier = trading_api.get_trading_mode_followed_strategy_signals_identifier(trading_mode)\n    if identifier is None:\n        return None\n    return authentication.Authenticator.instance().get_signal_community_url(\n        identifier\n    )\n\n\ndef is_community_feed_connected():\n    return authentication.Authenticator.instance().is_feed_connected()\n\n\ndef get_last_signal_time():\n    return authentication.Authenticator.instance().get_feed_last_message_time()\n\n\nasync def _sync_community_account():\n    profile_urls = await authentication.Authenticator.instance().get_subscribed_profile_urls()\n    return octobot_commands.download_missing_profiles(interfaces_util.get_edited_config(dict_only=False), profile_urls)\n\n\ndef sync_community_account():\n    return interfaces_util.run_in_bot_main_loop(_sync_community_account())\n\n\ndef wait_for_login_if_processing():\n    try:\n        interfaces_util.run_in_bot_main_loop(authentication.Authenticator.instance().wait_for_login_if_processing())\n    except asyncio.TimeoutError:\n        pass\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/configuration.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport logging\nimport os.path as path\nimport ccxt\nimport ccxt.async_support\nimport copy\nimport requests.adapters\nimport urllib3.util.retry\nimport typing\n\nimport gc\n\nimport octobot_evaluators.constants as evaluators_constants\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.api as evaluators_api\nimport octobot_services.api as services_api\nimport octobot_services.constants as services_constants\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_tentacles_manager.constants as tentacles_manager_constants\nimport octobot_trading.api as trading_api\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.exchanges as trading_exchanges\nimport octobot_trading.storage as trading_storage\nimport octobot_trading.enums as trading_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.logging as bot_logging\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.databases as commons_databases\nimport octobot_commons.configuration as configuration\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.authentication as authentication\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.display as display\nimport octobot_commons.errors as commons_errors\nimport octobot_commons.aiohttp_util as aiohttp_util\nimport octobot_commons.html_util as html_util\nimport octobot_commons\nimport octobot_backtesting.api as backtesting_api\nimport octobot.community as community\nimport octobot.constants as octobot_constants\nimport octobot.enums as octobot_enums\nimport octobot.configuration_manager as configuration_manager\nimport octobot.databases_util as octobot_databases_util\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.plugins as web_plugins\n\nNAME_KEY = \"name\"\nSHORT_NAME_KEY = \"n\"\nSYMBOL_KEY = \"s\"\nID_KEY = \"i\"\nEXCLUDED_CURRENCY_SUBNAME = tuple((\"X Long\", \"X Short\"))\nDESCRIPTION_KEY = \"description\"\nREQUIREMENTS_KEY = \"requirements\"\nCOMPATIBLE_TYPES_KEY = \"compatible-types\"\nREQUIREMENTS_COUNT_KEY = \"requirements-min-count\"\nDEFAULT_CONFIG_KEY = \"default-config\"\nTRADING_MODES_KEY = \"trading-modes\"\nSTRATEGIES_KEY = \"strategies\"\nTRADING_MODE_KEY = \"trading mode\"\nEXCHANGE_KEY = \"exchange\"\nWEB_PLUGIN_KEY = \"web plugin\"\nSTRATEGY_KEY = \"strategy\"\nTA_EVALUATOR_KEY = \"technical evaluator\"\nSOCIAL_EVALUATOR_KEY = \"social evaluator\"\nRT_EVALUATOR_KEY = \"real time evaluator\"\nSCRIPTED_EVALUATOR_KEY = \"scripted evaluator\"\nREQUIRED_KEY = \"required\"\nSOCIAL_KEY = \"social\"\nTA_KEY = \"ta\"\nRT_KEY = \"real-time\"\nSCRIPTED_KEY = \"scripted\"\nACTIVATED_STRATEGIES = \"activated_strategies\"\nBASE_CLASSES_KEY = \"base_classes\"\nEVALUATION_FORMAT_KEY = \"evaluation_format\"\nCONFIG_KEY = \"config\"\nDISPLAYED_ELEMENTS_KEY = \"displayed_elements\"\n\n# tentacles from which configuration is not handled in strategies / evaluators configuration and that can be groupped\nGROUPPABLE_NON_TRADING_STRATEGY_RELATED_TENTACLES = [\n    tentacles_manager_constants.TENTACLES_BACKTESTING_PATH,\n    tentacles_manager_constants.TENTACLES_SERVICES_PATH,\n    tentacles_manager_constants.TENTACLES_TRADING_PATH\n]\n# tentacles for which configuration can be done in the tentacles tab of profile config\nEXTRA_CONFIGURABLE_TENTACLES_TYPES = [\n    tentacles_manager_constants.TENTACLES_INTERFACES_PATH\n]\n_TENTACLE_CONFIG_CACHE = {}\n\nDEFAULT_EXCHANGE = \"binance\"\nMERGED_CCXT_EXCHANGES = {\n    result.__name__: [merged_exchange.__name__ for merged_exchange in merged]\n    for result, merged in (\n        (ccxt.async_support.kucoin, (ccxt.async_support.kucoinfutures, )),\n        (ccxt.async_support.binance, (ccxt.async_support.binanceusdm, ccxt.async_support.binancecoinm)),\n        (ccxt.async_support.htx, (ccxt.async_support.huobi, )),\n    )\n}\nREMOVED_CCXT_EXCHANGES = set().union(*(set(v) for v in MERGED_CCXT_EXCHANGES.values()))\n_FULL_EXCHANGE_LIST: typing.List[str] = None # type: ignore # should be accessed through get_or_init_FULL_EXCHANGE_LIST\nAUTO_FILLED_EXCHANGES = None\n\n\ndef _get_currency_dict(name, symbol, identifier):\n    return {\n        SHORT_NAME_KEY: name,\n        SYMBOL_KEY: symbol.upper(),\n        ID_KEY: identifier\n    }\n\n# buffers to faster config page loading\nmarkets_by_exchanges = {}\nall_symbols_dict = {}\nexchange_logos = {}\n# can't fetch symbols from coinmarketcap.com (which is in ccxt but is not an exchange and has a paid api)\nexchange_symbol_fetch_blacklist = {\"coinmarketcap\"}\n_LOGGER = None\n\ndef _get_logger():\n    global _LOGGER\n    if _LOGGER is None:\n        _LOGGER = bot_logging.get_logger(\"WebConfigurationModel\")\n    return _LOGGER\n\n\ndef _get_evaluators_tentacles_activation():\n    try:\n        return tentacles_manager_api.get_tentacles_activation(interfaces_util.get_edited_tentacles_config())[\n            tentacles_manager_constants.TENTACLES_EVALUATOR_PATH]\n    except KeyError:\n        return {}\n\n\ndef _get_trading_tentacles_activation():\n    try:\n        return tentacles_manager_api.get_tentacles_activation(interfaces_util.get_edited_tentacles_config())[\n            tentacles_manager_constants.TENTACLES_TRADING_PATH]\n    except KeyError:\n        return {}\n\n\ndef _get_services_tentacles_activation():\n    try:\n        return tentacles_manager_api.get_tentacles_activation(interfaces_util.get_edited_tentacles_config())[\n            tentacles_manager_constants.TENTACLES_SERVICES_PATH]\n    except KeyError:\n        return {}\n\n\ndef get_evaluators_tentacles_startup_activation():\n    try:\n        return tentacles_manager_api.get_tentacles_activation(interfaces_util.get_startup_tentacles_config())[\n            tentacles_manager_constants.TENTACLES_EVALUATOR_PATH]\n    except KeyError:\n        return {}\n\n\ndef get_trading_tentacles_startup_activation():\n    try:\n        return tentacles_manager_api.get_tentacles_activation(interfaces_util.get_startup_tentacles_config())[\n            tentacles_manager_constants.TENTACLES_TRADING_PATH]\n    except KeyError:\n        return {}\n\n\ndef get_tentacle_documentation(name, media_url, missing_tentacles: set = None):\n    try:\n        doc_content = tentacles_manager_api.get_tentacle_documentation(name)\n        if doc_content:\n            resource_url = \\\n                f\"{media_url}/{tentacles_manager_api.get_tentacle_resources_path(name).replace(path.sep, '/')}/\"\n            # patch resources paths into the tentacle resource path\n            return doc_content.replace(f\"\\n\\n\", \"<br><br>\")\\\n                .replace(f\"{tentacles_manager_constants.TENTACLE_RESOURCES}/\", resource_url)\n    except KeyError as e:\n        if missing_tentacles is None or name not in missing_tentacles:\n            _get_logger().error(f\"Impossible to load tentacle documentation for {name} ({e.__class__.__name__}: {e}). \"\n                                f\"This is probably an issue with the {name} tentacle matadata.json file, please \"\n                                f\"make sure this file is accurate and is referring {name} in the 'tentacles' list.\")\n        return \"\"\n    except TypeError:\n        # can happen when tentacles metadata.json are invalid\n        return \"\"\n\ndef _get_strategy_activation_state(\n        with_trading_modes, media_url, missing_tentacles: set, whitelist=None, backtestable_only=False\n):\n    import tentacles.Trading.Mode as modes\n    import tentacles.Evaluator.Strategies as strategies\n    strategy_config = {\n        TRADING_MODES_KEY: {},\n        STRATEGIES_KEY: {}\n    }\n    strategy_config_classes = {\n        TRADING_MODES_KEY: {},\n        STRATEGIES_KEY: {}\n    }\n\n    if with_trading_modes:\n        trading_config = _get_trading_tentacles_activation()\n        for key, val in trading_config.items():\n            if whitelist and key not in whitelist:\n                continue\n            config_class = tentacles_management.get_class_from_string(\n                key, trading_modes.AbstractTradingMode, modes, tentacles_management.trading_mode_parent_inspection\n            )\n            if config_class:\n                if not backtestable_only or (backtestable_only and config_class.is_backtestable()):\n                    strategy_config[TRADING_MODES_KEY][key] = {}\n                    strategy_config[TRADING_MODES_KEY][key][constants.ACTIVATION_KEY] = val\n                    strategy_config[TRADING_MODES_KEY][key][DESCRIPTION_KEY] = get_tentacle_documentation(\n                        key, media_url\n                    )\n                    strategy_config_classes[TRADING_MODES_KEY][key] = config_class\n            else:\n                _add_to_missing_tentacles_if_missing(key, missing_tentacles)\n\n    evaluator_config = _get_evaluators_tentacles_activation()\n    for key, val in evaluator_config.items():\n        if whitelist and key not in whitelist:\n            continue\n        config_class = tentacles_management.get_class_from_string(key, evaluators.StrategyEvaluator,\n                                                                  strategies,\n                                                                  tentacles_management.evaluator_parent_inspection)\n        if config_class:\n            strategy_config[STRATEGIES_KEY][key] = {}\n            strategy_config[STRATEGIES_KEY][key][constants.ACTIVATION_KEY] = val\n            strategy_config[STRATEGIES_KEY][key][DESCRIPTION_KEY] = get_tentacle_documentation(key, media_url)\n            strategy_config_classes[STRATEGIES_KEY][key] = config_class\n        else:\n            _add_to_missing_tentacles_if_missing(key, missing_tentacles)\n\n    return strategy_config, strategy_config_classes\n\n\ndef _add_to_missing_tentacles_if_missing(tentacle_name: str, missing_tentacles: set):\n    # if tentacle_name can't be accessed in tentacles manager, this tentacle is not available\n    try:\n        tentacles_manager_api.get_tentacle_version(tentacle_name)\n    except KeyError:\n        missing_tentacles.add(tentacle_name)\n    except AttributeError:\n        _get_logger().debug(f\"Missing tentacles data for {tentacle_name}. This is likely due to an error in the \"\n                            f\"associated metadata.json file.\")\n        missing_tentacles.add(tentacle_name)\n\n\ndef _get_tentacle_packages():\n    import tentacles.Trading.Mode as modes\n    yield modes, trading_modes.AbstractTradingMode, TRADING_MODE_KEY\n    import tentacles.Evaluator.Strategies as strategies\n    yield strategies, evaluators.StrategyEvaluator, STRATEGY_KEY\n    import tentacles.Evaluator.TA as ta\n    yield ta, evaluators.AbstractEvaluator, TA_EVALUATOR_KEY\n    import tentacles.Evaluator.Social as social\n    yield social, evaluators.AbstractEvaluator, SOCIAL_EVALUATOR_KEY\n    import tentacles.Evaluator.RealTime as rt\n    yield rt, evaluators.AbstractEvaluator, RT_EVALUATOR_KEY\n    import tentacles.Evaluator.Scripted as scripted\n    yield scripted, evaluators.ScriptedEvaluator, SCRIPTED_EVALUATOR_KEY\n    import tentacles.Trading.Exchange as exchanges\n    yield exchanges, trading_exchanges.AbstractExchange, EXCHANGE_KEY\n    import tentacles.Services.Interfaces as interfaces\n    yield interfaces, web_plugins.AbstractWebInterfacePlugin, WEB_PLUGIN_KEY\n\n\ndef _get_activation_state(name, activation_states):\n    return name in activation_states and activation_states[name]\n\n\ndef is_trading_strategy_configuration(tentacle_type):\n    return tentacle_type in (\n        SCRIPTED_EVALUATOR_KEY, RT_EVALUATOR_KEY, SOCIAL_EVALUATOR_KEY, TA_EVALUATOR_KEY, STRATEGY_KEY, TRADING_MODE_KEY\n    )\n\n\ndef get_tentacle_from_string(name, media_url, with_info=True):\n    for package, abstract_class, tentacle_type in _get_tentacle_packages():\n        parent_inspector = tentacles_management.evaluator_parent_inspection\n        if tentacle_type == TRADING_MODE_KEY:\n            parent_inspector = tentacles_management.trading_mode_parent_inspection\n        if tentacle_type in (EXCHANGE_KEY, WEB_PLUGIN_KEY):\n            parent_inspector = tentacles_management.default_parents_inspection\n        klass = tentacles_management.get_class_from_string(name, abstract_class, package, parent_inspector)\n        if klass:\n            if with_info:\n                info = {\n                    DESCRIPTION_KEY: get_tentacle_documentation(name, media_url),\n                    NAME_KEY: name\n                }\n                if tentacle_type == TRADING_MODE_KEY:\n                    _add_trading_mode_requirements_and_default_config(info, klass)\n                    activation_states = _get_trading_tentacles_activation()\n                elif tentacle_type == EXCHANGE_KEY:\n                    activation_states = _get_trading_tentacles_activation()\n                elif tentacle_type == WEB_PLUGIN_KEY:\n                    activation_states = _get_services_tentacles_activation()\n                else:\n                    activation_states = _get_evaluators_tentacles_activation()\n                    if tentacle_type == STRATEGY_KEY:\n                        _add_strategy_requirements_and_default_config(info, klass)\n                info[constants.ACTIVATION_KEY] = _get_activation_state(name, activation_states)\n                return klass, tentacle_type, info\n            else:\n                return klass, tentacle_type, None\n    return None, None, None\n\n\ndef get_tentacle_user_commands(klass):\n    return klass.get_user_commands() if klass is not None and hasattr(klass, \"get_user_commands\") else {}\n\n\nasync def get_tentacle_config_and_user_inputs(tentacle_class, bot_config, tentacles_setup_config):\n    return await tentacle_class.get_raw_config_and_user_inputs(\n        bot_config,\n        tentacles_setup_config,\n        interfaces_util.get_bot_api().get_bot_id()\n    )\n\n\ndef get_tentacle_config_and_edit_display(tentacle, tentacle_class=None, profile_id=None):\n    config = interfaces_util.get_edited_config()\n    tentacles_setup_config = interfaces_util.get_edited_tentacles_config()\n    if profile_id:\n        config = models.get_profile(profile_id).config\n        tentacles_setup_config = models.get_tentacles_setup_config_from_profile_id(profile_id)\n    tentacle_class = tentacle_class or tentacles_manager_api.get_tentacle_class_from_string(tentacle)\n    config, user_inputs = interfaces_util.run_in_bot_main_loop(\n        get_tentacle_config_and_user_inputs(tentacle_class, config, tentacles_setup_config)\n    )\n    display_elements = display.display_translator_factory()\n    display_elements.add_user_inputs(user_inputs)\n    return {\n        NAME_KEY: tentacle,\n        CONFIG_KEY: config or {},\n        DISPLAYED_ELEMENTS_KEY: display_elements.to_json()\n    }\n\n\ndef are_automations_enabled():\n    return octobot_constants.ENABLE_AUTOMATIONS\n\n\ndef is_advanced_interface_enabled():\n    return octobot_constants.ENABLE_ADVANCED_INTERFACE\n\n\ndef restart_global_automations():\n    interfaces_util.run_in_bot_main_loop(\n        interfaces_util.get_bot_api().get_automation().restart(),\n        log_exceptions=False\n    )\n\n\ndef get_all_automation_steps():\n    return interfaces_util.get_bot_api().get_automation().get_all_steps()\n\n\ndef has_at_least_one_running_automation():\n    return bool(get_automations_count())\n\n\ndef get_automations_count():\n    return len(interfaces_util.get_bot_api().get_automation().automation_details)\n\n\ndef reset_automation_config_to_default():\n    try:\n        interfaces_util.get_bot_api().get_automation().reset_config()\n        return True, f\"{interfaces_util.get_bot_api().get_automation().get_name()} configuration reset to default values\"\n    except Exception as err:\n        return False, str(err)\n\n\ndef get_tentacle_config(klass):\n    return tentacles_manager_api.get_tentacle_config(interfaces_util.get_edited_tentacles_config(), klass)\n\n\ndef get_cached_tentacle_config(klass):\n    \"\"\"\n    Should only be used to read static parts of a tentacle config (like requirements)\n    \"\"\"\n    key = klass if isinstance(klass, str) else klass.get_name()\n    try:\n        return _TENTACLE_CONFIG_CACHE[key]\n    except KeyError:\n        _TENTACLE_CONFIG_CACHE[key] = get_tentacle_config(klass)\n    return _TENTACLE_CONFIG_CACHE[key]\n\n\ndef get_tentacle_config_schema(klass):\n    try:\n        _get_logger().error(\"get_tentacle_config_schema\")\n        with open(tentacles_manager_api.get_tentacle_config_schema_path(klass)) as schema_file:\n            return schema_file.read()\n    except Exception:\n        return \"\"\n\n\ndef _get_tentacle_activation_desc(name, activated, startup_val, media_url, missing_tentacles: set):\n    return {\n        constants.TENTACLE_CLASS_NAME: name,\n        constants.ACTIVATION_KEY: activated,\n        DESCRIPTION_KEY: get_tentacle_documentation(name, media_url, missing_tentacles),\n        constants.STARTUP_CONFIG_KEY: startup_val\n    }\n\n\ndef _add_tentacles_activation_desc_for_group(activation_by_group, tentacles_activation, startup_tentacles_activation,\n                                             root_element, media_url, missing_tentacles: set):\n    for tentacle_class_name, activated in tentacles_activation.get(root_element, {}).items():\n        startup_val = startup_tentacles_activation[root_element][tentacle_class_name]\n        try:\n            tentacle = _get_tentacle_activation_desc(tentacle_class_name, activated, startup_val, media_url,\n                                                     missing_tentacles)\n            group = tentacles_manager_api.get_tentacle_group(tentacle_class_name)\n            if group in activation_by_group:\n                activation_by_group[group].append(tentacle)\n            else:\n                activation_by_group[group] = [tentacle]\n        except AttributeError:\n            # can happen when tentacles metadata.json are invalid\n            pass\n\n\ndef get_extra_tentacles_config_desc(media_url, missing_tentacles: set):\n    tentacles_descriptions = []\n    all_tentacles = {\n        tentacle_class.__name__: tentacle_class\n        for tentacle_class in tentacles_management.AbstractTentacle.get_all_subclasses()\n    }\n    for tentacle_type in EXTRA_CONFIGURABLE_TENTACLES_TYPES:\n        for tentacle_class_name in tentacles_manager_api.get_tentacles_classes_names_for_type(tentacle_type):\n            if tentacle_class_name in all_tentacles and all_tentacles[tentacle_class_name].is_configurable():\n                try:\n                    tentacles_descriptions.append(\n                        _get_tentacle_activation_desc(\n                            tentacle_class_name, True, True, media_url, missing_tentacles\n                        )\n                    )\n                except AttributeError:\n                    # can happen when tentacles metadata.json are invalid\n                    pass\n    return tentacles_descriptions\n\n\ndef get_tentacles_activation_desc_by_group(media_url, missing_tentacles: set):\n    tentacles_activation = tentacles_manager_api.get_tentacles_activation(interfaces_util.get_edited_tentacles_config())\n    startup_tentacles_activation = tentacles_manager_api.get_tentacles_activation(\n        interfaces_util.get_startup_tentacles_config())\n    activation_by_group = {}\n    for root_element in GROUPPABLE_NON_TRADING_STRATEGY_RELATED_TENTACLES:\n        try:\n            _add_tentacles_activation_desc_for_group(activation_by_group, tentacles_activation,\n                                                     startup_tentacles_activation, root_element, media_url,\n                                                     missing_tentacles)\n        except KeyError:\n            pass\n    # only return tentacle groups for which there is an activation choice to simplify the config interface\n    return {group: tentacles\n            for group, tentacles in activation_by_group.items()\n            if len(tentacles) > 1}\n\n\ndef update_tentacle_config(tentacle_name, config_update, tentacle_class=None, tentacles_setup_config=None):\n    try:\n        tentacle_class = tentacle_class or get_tentacle_from_string(tentacle_name, None, with_info=False)[0]\n        if tentacle_class is None:\n            return False, f\"Can't find {tentacle_name} class\"\n        tentacles_manager_api.update_tentacle_config(\n            tentacles_setup_config or interfaces_util.get_edited_tentacles_config(),\n            tentacle_class,\n            config_update\n        )\n        return True, f\"{tentacle_name} updated\"\n    except Exception as e:\n        _get_logger().exception(e, False)\n        return False, f\"Error when updating tentacle config: {e}\"\n\n\ndef update_copied_trading_id(copy_id):\n    import tentacles.Trading.Mode as modes\n    return update_tentacle_config(\n        modes.RemoteTradingSignalsTradingMode.get_name(),\n        {\n            \"trading_strategy\": copy_id\n        }\n    )\n\n\ndef reset_config_to_default(tentacle_name, tentacle_class=None, tentacles_setup_config=None):\n    try:\n        tentacle_class = tentacle_class or get_tentacle_from_string(tentacle_name, None, with_info=False)[0]\n        tentacles_manager_api.factory_tentacle_reset_config(\n            tentacles_setup_config or interfaces_util.get_edited_tentacles_config(),\n            tentacle_class\n        )\n        return True, f\"{tentacle_name} configuration reset to default values\"\n    except FileNotFoundError as e:\n        error_message = f\"Error when resetting factory tentacle config: no default values file at {e.filename}\"\n        _get_logger().error(error_message)\n        return False, error_message\n    except Exception as e:\n        _get_logger().exception(e, False)\n        return False, f\"Error when resetting factory tentacle config: {e}\"\n\n\ndef _get_required_element(elements_config):\n    requirements = REQUIREMENTS_KEY\n    required_elements = set()\n    for element_type in elements_config.values():\n        for element_name, element in element_type.items():\n            if element[constants.ACTIVATION_KEY]:\n                if requirements in element:\n                    required_elements = required_elements.union(element[requirements])\n    return required_elements\n\n\ndef _add_strategy_requirements_and_default_config(desc, klass):\n    tentacles_config = interfaces_util.get_startup_tentacles_config()\n    strategy_config = get_cached_tentacle_config(klass)\n    desc[REQUIREMENTS_KEY] = [evaluator for evaluator in klass.get_required_evaluators(tentacles_config,\n                                                                                       strategy_config)]\n    desc[COMPATIBLE_TYPES_KEY] = [evaluator for evaluator in klass.get_compatible_evaluators_types(tentacles_config,\n                                                                                                   strategy_config)]\n    desc[DEFAULT_CONFIG_KEY] = [evaluator for evaluator in klass.get_default_evaluators(tentacles_config,\n                                                                                        strategy_config)]\n\n\ndef _add_trading_mode_requirements_and_default_config(desc, klass):\n    tentacles_config = interfaces_util.get_startup_tentacles_config()\n    mode_config = get_cached_tentacle_config(klass)\n    required_strategies, required_strategies_count = klass.get_required_strategies_names_and_count(tentacles_config,\n                                                                                                   mode_config)\n    if required_strategies:\n        desc[REQUIREMENTS_KEY] = \\\n            [strategy for strategy in required_strategies]\n        desc[DEFAULT_CONFIG_KEY] = \\\n            [strategy for strategy in klass.get_default_strategies(tentacles_config, mode_config)]\n        desc[REQUIREMENTS_COUNT_KEY] = required_strategies_count\n    else:\n        desc[REQUIREMENTS_KEY] = []\n        desc[REQUIREMENTS_COUNT_KEY] = 0\n\n\ndef _add_strategies_requirements(strategies, strategy_config):\n    required_elements = _get_required_element(strategy_config)\n    for classKey, klass in strategies.items():\n        _add_strategy_requirements_and_default_config(strategy_config[STRATEGIES_KEY][classKey], klass)\n        strategy_config[STRATEGIES_KEY][classKey][REQUIRED_KEY] = classKey in required_elements\n\n\ndef _add_trading_modes_requirements(trading_modes_list, strategy_config):\n    for classKey, klass in trading_modes_list.items():\n        try:\n            _add_trading_mode_requirements_and_default_config(strategy_config[TRADING_MODES_KEY][classKey], klass)\n        except Exception as e:\n            _get_logger().exception(e, False)\n\n\ndef get_strategy_config(\n        media_url, missing_tentacles: set, with_trading_modes=True, whitelist=None, backtestable_only=False\n):\n    strategy_config, strategy_config_classes = _get_strategy_activation_state(with_trading_modes,\n                                                                              media_url,\n                                                                              missing_tentacles,\n                                                                              whitelist=whitelist,\n                                                                              backtestable_only=backtestable_only)\n    if with_trading_modes:\n        _add_trading_modes_requirements(strategy_config_classes[TRADING_MODES_KEY], strategy_config)\n    _add_strategies_requirements(strategy_config_classes[STRATEGIES_KEY], strategy_config)\n    return strategy_config\n\n\ndef get_in_backtesting_mode():\n    return backtesting_api.is_backtesting_enabled(interfaces_util.get_global_config())\n\n\ndef accepted_terms():\n    return interfaces_util.get_edited_config(dict_only=False).accepted_terms()\n\n\ndef accept_terms(accepted):\n    return interfaces_util.get_edited_config(dict_only=False).accept_terms(accepted)\n\n\ndef _fill_evaluator_config(evaluator_name, activated, eval_type_key,\n                           evaluator_type, detailed_config, media_url, name_filter=None):\n    klass = tentacles_management.get_class_from_string(evaluator_name, evaluators.AbstractEvaluator, evaluator_type,\n                                                       tentacles_management.evaluator_parent_inspection)\n    filtered = name_filter and evaluator_name != name_filter\n    if klass:\n        if not filtered:\n            detailed_config[eval_type_key][evaluator_name] = {}\n            detailed_config[eval_type_key][evaluator_name][constants.ACTIVATION_KEY] = activated\n            detailed_config[eval_type_key][evaluator_name][DESCRIPTION_KEY] = \\\n                get_tentacle_documentation(evaluator_name, media_url)\n            detailed_config[eval_type_key][evaluator_name][EVALUATION_FORMAT_KEY] = \"float\" \\\n                if klass.get_eval_type() == evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE \\\n                else str(klass.get_eval_type())\n        return True, klass, filtered\n    return False, klass, filtered\n\n\ndef get_evaluator_detailed_config(media_url, missing_tentacles: set, single_strategy=None):\n    import tentacles.Evaluator.Strategies as strategies\n    import tentacles.Evaluator.TA as ta\n    import tentacles.Evaluator.Social as social\n    import tentacles.Evaluator.RealTime as rt\n    import tentacles.Evaluator.Scripted as scripted\n    detailed_config = {\n        SOCIAL_KEY: {},\n        TA_KEY: {},\n        RT_KEY: {},\n        SCRIPTED_KEY: {}\n    }\n    strategy_config = {\n        STRATEGIES_KEY: {}\n    }\n    strategy_class_by_name = {}\n    evaluator_config = _get_evaluators_tentacles_activation()\n    for evaluator_name, activated in evaluator_config.items():\n        is_TA, klass, _ = _fill_evaluator_config(evaluator_name, activated, TA_KEY, ta, detailed_config, media_url)\n        if not is_TA:\n            is_social, klass, _ = _fill_evaluator_config(evaluator_name, activated, SOCIAL_KEY,\n                                                         social, detailed_config, media_url)\n            if not is_social:\n                is_real_time, klass, _ = _fill_evaluator_config(evaluator_name, activated, RT_KEY,\n                                                                rt, detailed_config, media_url)\n                if not is_real_time:\n                    is_scripted, klass, _ = _fill_evaluator_config(evaluator_name, activated, SCRIPTED_KEY,\n                                                                   scripted, detailed_config, media_url)\n                    if not is_scripted:\n                        is_strategy, klass, filtered = _fill_evaluator_config(evaluator_name, activated, STRATEGIES_KEY,\n                                                                              strategies, strategy_config, media_url,\n                                                                              name_filter=single_strategy)\n                        if is_strategy:\n                            if not filtered:\n                                strategy_class_by_name[evaluator_name] = klass\n                        else:\n                            _add_to_missing_tentacles_if_missing(evaluator_name, missing_tentacles)\n\n    _add_strategies_requirements(strategy_class_by_name, strategy_config)\n    if required_elements := _get_required_element(strategy_config):\n        for eval_type in detailed_config.values():\n            for eval_name, eval_details in eval_type.items():\n                eval_details[REQUIRED_KEY] = eval_name in required_elements\n\n    detailed_config[ACTIVATED_STRATEGIES] = [\n        s\n        for s, details in strategy_config[STRATEGIES_KEY].items()\n        if details[constants.ACTIVATION_KEY]\n    ]\n    return detailed_config\n\n\ndef get_config_activated_trading_mode(tentacles_setup_config=None):\n    try:\n        return trading_api.get_activated_trading_mode(\n            tentacles_setup_config or interfaces_util.get_bot_api().get_edited_tentacles_config()\n        )\n    except commons_errors.ConfigTradingError:\n        return None\n\n\ndef get_config_activated_strategies(tentacles_setup_config=None):\n    return evaluators_api.get_activated_strategies_classes(\n        tentacles_setup_config or interfaces_util.get_bot_api().get_edited_tentacles_config()\n    )\n\n\ndef get_config_activated_evaluators(tentacles_setup_config=None):\n    return evaluators_api.get_activated_evaluators(\n        tentacles_setup_config or interfaces_util.get_bot_api().get_edited_tentacles_config()\n    )\n\n\ndef has_futures_exchange():\n    for exchange_manager in get_live_trading_enabled_exchange_managers():\n        if trading_api.get_exchange_type(exchange_manager) is trading_enums.ExchangeTypes.FUTURE:\n            return True\n    return False\n\n\ndef update_tentacles_activation_config(new_config, deactivate_others=False, tentacles_setup_configuration=None):\n    tentacles_setup_configuration = tentacles_setup_configuration or interfaces_util.get_edited_tentacles_config()\n    try:\n        updated_config = {\n            element_name: activated if isinstance(activated, bool) else activated.lower() == \"true\"\n            for element_name, activated in new_config.items()\n        }\n        if tentacles_manager_api.update_activation_configuration(\n                tentacles_setup_configuration, updated_config, deactivate_others\n        ):\n            tentacles_manager_api.save_tentacles_setup_configuration(tentacles_setup_configuration)\n        return True\n    except Exception as e:\n        _get_logger().exception(e, True, f\"Error when updating tentacles activation {e}\")\n        return False\n\n\ndef get_active_exchanges():\n    return trading_api.get_enabled_exchanges_names(interfaces_util.get_startup_config(dict_only=True))\n\n\nasync def _reset_profile_portfolio_history(current_edited_config):\n    models.clear_exchanges_portfolio_history(simulated_only=True)\n    if not trading_api.is_trader_simulator_enabled_in_config(current_edited_config.config):\n        return\n    # also reset portfolio history for exchanges enabled in config that are not enabled in the current instance\n    already_reset_exchanges = {\n        trading_api.get_exchange_name(exchange_manager): exchange_manager\n        for exchange_manager in trading_api.get_exchange_managers_from_exchange_ids(trading_api.get_exchange_ids())\n    }\n    run_dbs_identifier = octobot_databases_util.get_run_databases_identifier(\n        current_edited_config.config,\n        interfaces_util.get_edited_tentacles_config()\n    )\n    enabled_exchanges = trading_api.get_enabled_exchanges_names(current_edited_config.config)\n    _get_logger().info(f\"Resetting simulated portfolio history for {enabled_exchanges}.\")\n    for exchange in enabled_exchanges:\n        for is_future in (True, False):\n            # force reset future and non future historical portfolio\n            if exchange not in already_reset_exchanges \\\n                    or ((trading_api.get_exchange_type(already_reset_exchanges[exchange])\n                        == trading_enums.ExchangeTypes.FUTURE) != is_future):\n                metadb = commons_databases.MetaDatabase(run_dbs_identifier)\n                portfolio_db = metadb.get_historical_portfolio_value_db(\n                    trading_api.get_account_type(is_future, False, False, True), exchange\n                )\n                await trading_api.clear_database_storage_history(\n                    trading_storage.PortfolioStorage, portfolio_db, False\n                )\n                await metadb.close()\n\n\ndef _handle_special_fields(current_edited_config, new_config):\n    config = current_edited_config.config\n    try:\n        # replace web interface password by its hash before storage\n        web_password_key = constants.UPDATED_CONFIG_SEPARATOR.join([services_constants.CONFIG_CATEGORY_SERVICES,\n                                                                    services_constants.CONFIG_WEB,\n                                                                    services_constants.CONFIG_WEB_PASSWORD])\n        if web_password_key in new_config:\n            new_config[web_password_key] = configuration.get_password_hash(new_config[web_password_key])\n        # add exchange enabled param if missing\n        for key in list(new_config.keys()):\n            values = key.split(constants.UPDATED_CONFIG_SEPARATOR)\n            if values[0] == commons_constants.CONFIG_EXCHANGES and \\\n                    values[1] not in config[commons_constants.CONFIG_EXCHANGES]:\n                enabled_key = constants.UPDATED_CONFIG_SEPARATOR.join([commons_constants.CONFIG_EXCHANGES,\n                                                                       values[1],\n                                                                       commons_constants.CONFIG_ENABLED_OPTION])\n                if enabled_key not in new_config:\n                    new_config[enabled_key] = True\n    except KeyError:\n        pass\n\n\ndef _handle_simulated_portfolio(current_edited_config, new_config):\n    # reset portfolio history if simulated portfolio has changed\n    if any(\n            f\"{commons_constants.CONFIG_SIMULATOR}{constants.UPDATED_CONFIG_SEPARATOR}\" \\\n            f\"{commons_constants.CONFIG_STARTING_PORTFOLIO}\" in key\n            for key in new_config\n    ):\n        try:\n            interfaces_util.run_in_bot_async_executor(\n                _reset_profile_portfolio_history(current_edited_config)\n            )\n        except Exception as err:\n            _get_logger().exception(err, True, f\"Error when resetting portfolio simulator history {err}\")\n\n\ndef update_global_config(new_config, delete=False):\n    try:\n        current_edited_config = interfaces_util.get_edited_config(dict_only=False)\n        if not delete:\n            _handle_special_fields(current_edited_config, new_config)\n        current_edited_config.update_config_fields(new_config,\n                                                   backtesting_api.is_backtesting_enabled(current_edited_config.config),\n                                                   constants.UPDATED_CONFIG_SEPARATOR,\n                                                   delete=delete)\n        _handle_simulated_portfolio(current_edited_config, new_config)\n        return True, \"\"\n    except Exception as e:\n        _get_logger().exception(e, True, f\"Error when updating global config {e}\")\n        return False, str(e)\n\n\ndef activate_metrics(enable_metrics):\n    current_edited_config = interfaces_util.get_edited_config(dict_only=False)\n    if commons_constants.CONFIG_METRICS not in current_edited_config.config:\n        current_edited_config.config[commons_constants.CONFIG_METRICS] = {\n            commons_constants.CONFIG_ENABLED_OPTION: enable_metrics}\n    else:\n        current_edited_config.config[commons_constants.CONFIG_METRICS][\n            commons_constants.CONFIG_ENABLED_OPTION] = enable_metrics\n    if enable_metrics and community.CommunityManager.should_register_bot(current_edited_config):\n        community.CommunityManager.background_get_id_and_register_bot(interfaces_util.get_bot_api())\n    current_edited_config.save()\n\n\ndef activate_beta_env(enable_beta):\n    new_env = octobot_enums.CommunityEnvironments.Staging if enable_beta \\\n        else octobot_enums.CommunityEnvironments.Production\n    current_edited_config = interfaces_util.get_edited_config(dict_only=False)\n    if octobot_constants.CONFIG_COMMUNITY not in current_edited_config.config:\n        current_edited_config.config[octobot_constants.CONFIG_COMMUNITY] = {}\n    current_edited_config.config[octobot_constants.CONFIG_COMMUNITY][\n        octobot_constants.CONFIG_COMMUNITY_ENVIRONMENT] = new_env.value\n    current_edited_config.save()\n\n\ndef get_metrics_enabled():\n    return interfaces_util.get_edited_config(dict_only=False).get_metrics_enabled()\n\n\ndef get_beta_env_enabled_in_config():\n    return octobot_constants.USE_BETA_EARLY_ACCESS or community.IdentifiersProvider.is_staging_environment_enabled(\n        interfaces_util.get_edited_config(dict_only=True)\n    )\n\n\ndef get_services_list():\n    services = {}\n    for service in services_api.get_available_services():\n        srv = service.instance()\n        if srv.get_required_config():\n            # do not add services without a config, ex: GoogleService (nothing to show on the web interface)\n            services[srv.get_type()] = srv\n    return services\n\n\ndef get_notifiers_list():\n    return [service.instance().get_type()\n            for notifier in services_api.create_notifier_factory({}).get_available_notifiers()\n            for service in notifier.REQUIRED_SERVICES]\n\n\ndef get_enabled_trading_pairs() -> set:\n    symbols = set()\n    for values in format_config_symbols(interfaces_util.get_edited_config()).values():\n        if values[commons_constants.CONFIG_ENABLED_OPTION]:\n            symbols = symbols.union(set(values[commons_constants.CONFIG_CRYPTO_PAIRS]))\n    return symbols\n\n\ndef get_exchange_available_trading_pairs(exchange_manager, profile=None) -> list:\n    return trading_api.get_trading_pairs(exchange_manager) if profile is None else [\n        pair\n        for pair in trading_api.get_all_exchange_symbols(exchange_manager)\n        if pair in trading_api.get_config_symbols(profile.config, True)\n    ]\n\n\ndef get_symbol_list(exchanges):\n    result = interfaces_util.run_in_bot_async_executor(_load_markets(exchanges))\n    return list(set(result))\n\n\ndef get_all_currencies(exchanges):\n    symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in get_symbol_list(exchanges)\n    ]\n    return list(\n        set(symbol.base for symbol in symbols).union(set(symbol.quote for symbol in symbols))\n    )\n\n\ndef _get_filtered_exchange_symbols(symbols):\n    return [res for res in symbols if octobot_commons.MARKET_SEPARATOR in res]\n\n\nasync def _load_market(exchange, results):\n    try:\n        if exchange in auto_filled_exchanges():\n            async with trading_api.get_new_ccxt_client(\n                exchange, {}, interfaces_util.get_edited_tentacles_config(), False\n            ) as client:\n                await client.load_markets()\n                symbols = client.symbols\n        else:\n            async with getattr(ccxt.async_support, exchange)({'verbose': False}) as client:\n                client.logger.setLevel(logging.INFO)    # prevent log of each request (huge on market statuses)\n                await client.load_markets()\n                symbols = client.symbols\n        # filter symbols with a \".\" or no \"/\" because bot can't handle them for now\n        markets_by_exchanges[exchange] = _get_filtered_exchange_symbols(symbols)\n        results.append(markets_by_exchanges[exchange])\n    except Exception as e:\n        _get_logger().exception(e, True, f\"error when loading symbol list for {exchange}: {e}\")\n\n\ndef _add_merged_exchanges(exchanges):\n    extended = list(exchanges)\n    for exchange in exchanges:\n        if exchange in MERGED_CCXT_EXCHANGES:\n            for merged_exchange in MERGED_CCXT_EXCHANGES[exchange]:\n                extended.append(merged_exchange)\n    return extended\n\n\nasync def _load_markets(exchanges):\n    result = []\n    results = []\n    fetch_coros = []\n    exchange_managers = trading_api.get_exchange_managers_from_exchange_ids(\n        trading_api.get_exchange_ids()\n    )\n    exchange_manager_by_exchange_name = {\n        trading_api.get_exchange_name(exchange_manager): exchange_manager\n        for exchange_manager in exchange_managers\n        if not trading_api.get_is_backtesting(exchange_manager)\n    }\n    for exchange in _add_merged_exchanges(exchanges):\n        if exchange not in exchange_symbol_fetch_blacklist:\n            if exchange in exchange_manager_by_exchange_name and exchange not in markets_by_exchanges:\n                markets_by_exchanges[exchange] = _get_filtered_exchange_symbols(\n                    trading_api.get_all_exchange_symbols(exchange_manager_by_exchange_name[exchange])\n                )\n            if exchange in markets_by_exchanges:\n                result += markets_by_exchanges[exchange]\n            else:\n                fetch_coros.append(_load_market(exchange, results))\n    if fetch_coros:\n        await asyncio.gather(*fetch_coros)\n        for res in results:\n            result += res\n    return result\n\n\ndef get_config_time_frames() -> list:\n    return time_frame_manager.get_config_time_frame(interfaces_util.get_global_config())\n\n\ndef get_timeframes_list(exchanges):\n    timeframes_list = []\n    allowed_timeframes = set(tf.value for tf in commons_enums.TimeFrames)\n    for exchange in exchanges:\n        if exchange not in exchange_symbol_fetch_blacklist:\n            timeframes_list += interfaces_util.run_in_bot_async_executor(\n                    trading_api.get_ccxt_exchange_available_time_frames(\n                        exchange, interfaces_util.get_edited_tentacles_config()\n                    ))\n    return [commons_enums.TimeFrames(time_frame)\n            for time_frame in list(set(timeframes_list))\n            if time_frame in allowed_timeframes]\n\n\ndef get_strategy_required_time_frames(strategy_class, tentacles_setup_config=None):\n    return strategy_class.get_required_time_frames(\n        {},\n        tentacles_setup_config or interfaces_util.get_edited_tentacles_config()\n    )\n\n\ndef format_config_symbols(config):\n    for currency, data in config[commons_constants.CONFIG_CRYPTO_CURRENCIES].items():\n        if commons_constants.CONFIG_ENABLED_OPTION not in data:\n            config[commons_constants.CONFIG_CRYPTO_CURRENCIES][currency] = \\\n                {**{commons_constants.CONFIG_ENABLED_OPTION: True}, **data}\n    return config[commons_constants.CONFIG_CRYPTO_CURRENCIES]\n\n\ndef format_config_symbols_without_enabled_key(config):\n    enabled_config = {}\n    for currency, data in config[commons_constants.CONFIG_CRYPTO_CURRENCIES].items():\n        if data.get(commons_constants.CONFIG_ENABLED_OPTION, False) and data[commons_constants.CONFIG_CRYPTO_PAIRS]:\n            enabled_config[currency] = {\n                commons_constants.CONFIG_CRYPTO_PAIRS: data[commons_constants.CONFIG_CRYPTO_PAIRS]\n            }\n    return enabled_config\n\n\ndef _is_legit_currency(currency):\n    return not any(sub_name in currency for sub_name in EXCLUDED_CURRENCY_SUBNAME) and len(currency) < 30\n\n\ndef get_all_symbols_list():\n    import tentacles.Services.Interfaces.web_interface.flask_util as flask_util\n    data_provider = flask_util.BrowsingDataProvider.instance()\n    all_currencies = copy.copy(data_provider.get_all_currencies())\n    if not all_currencies:\n        added_is = set()\n        request_response = None\n        base_error = \"Failed to get currencies list from coingecko.com (this is a display only issue): \"\n        try:\n            # inspired from https://github.com/man-c/pycoingecko\n            session = requests.Session()\n            retries = urllib3.util.retry.Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504])\n            session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries))\n            # first fetch top 250 currencies then add all currencies and their ids\n            for url in (f\"{constants.CURRENCIES_LIST_URL}1\", constants.ALL_SYMBOLS_URL):\n                request_response = session.get(url)\n                if request_response.status_code == 429:\n                    # rate limit issue\n                    _get_logger().warning(f\"{base_error}Too many requests, retry in a few seconds\")\n                    break\n                for currency_data in request_response.json():\n                    if _is_legit_currency(currency_data[NAME_KEY]):\n                        currency_id = currency_data[\"id\"]\n                        if currency_id not in added_is:\n                            added_is.add(currency_id)\n                            all_currencies.append(_get_currency_dict(\n                                currency_data[NAME_KEY],\n                                currency_data[\"symbol\"],\n                                currency_id\n                            ))\n            # fetched_all: save it\n            data_provider.set_all_currencies(all_currencies)\n        except Exception as e:\n            str_error = html_util.get_html_summary_if_relevant(e)\n            details = f\"code: {request_response.status_code}, error: {str_error}\" \\\n                if request_response else {request_response}\n            _get_logger().exception(e, True, f\"{base_error}{str_error}\")\n            _get_logger().debug(f\"coingecko.com response {details}\")\n            return {}\n    return all_currencies\n\n\ndef get_all_symbols_list_by_symbol_type(all_symbols, config_symbols):\n    spot = \"SPOT trading\"\n    linear = \"Futures trading - linear\"\n    inverse = \"Futures trading - inverse\"\n\n    def _is_of_type(symbol, trading_type):\n        parsed = commons_symbols.parse_symbol(symbol)\n        if parsed.is_spot():\n            return trading_type == spot\n        elif parsed.is_perpetual_future():\n            if trading_type == linear:\n                return parsed.is_linear()\n            if trading_type == inverse:\n                return parsed.is_inverse()\n        return False\n    symbols_by_type = {\n        trading_type: [symbol for symbol in all_symbols if _is_of_type(symbol, trading_type)]\n        for trading_type in (\n            spot, linear, inverse\n        )\n    }\n    symbols_in_config = set().union(*(\n        set(currency_details.get('pairs', [])) for currency_details in config_symbols.values()\n    ))\n    if symbols_in_config:\n        listed_symbols = set().union(*(set(symbols) for symbols in symbols_by_type.values()))\n        missing_symbols = symbols_in_config - listed_symbols\n        if missing_symbols:\n            symbols_by_type[\"Configured (missing on enabled exchanges)\"] = list(missing_symbols)\n    return symbols_by_type\n\n\ndef get_exchange_logo(exchange_name):\n    try:\n        return exchange_logos[exchange_name]\n    except KeyError:\n        try:\n            exchange_logos[exchange_name] = {\"image\": \"\", \"url\": \"\"}\n            if isinstance(exchange_name, str) and exchange_name != \"Bitcoin\":\n                exchange_details = interfaces_util.run_in_bot_main_loop(\n                    trading_api.get_exchange_details(\n                        exchange_name,\n                        exchange_name in auto_filled_exchanges(),\n                        interfaces_util.get_edited_tentacles_config(),\n                        interfaces_util.get_bot_api().get_aiohttp_session()\n                    )\n                )\n                exchange_logos[exchange_name][\"image\"] = exchange_details.logo_url\n                exchange_logos[exchange_name][\"url\"] = exchange_details.url\n        except KeyError:\n            pass\n    return exchange_logos[exchange_name]\n\n\ndef _get_currency_logo_url(currency_id):\n    return f\"https://api.coingecko.com/api/v3/coins/{currency_id}?localization=false&tickers=false&market_data=\" \\\n           f\"false&community_data=false&developer_data=false&sparkline=false\"\n\n\nasync def _fetch_currency_logo(session, data_provider, currency_id):\n    if not currency_id:\n        return\n    async with session.get(_get_currency_logo_url(currency_id)) as resp:\n        logo = None\n        try:\n            json_resp = await resp.json()\n            logo = json_resp[\"image\"][\"large\"]\n        except KeyError:\n            if resp.status == 429:\n                _get_logger().debug(f\"Rate limitted when trying to fetch logo for {currency_id}. Will retry later\")\n            else:\n                # not rate limit: problem\n                _get_logger().warning(f\"Unexpected error when fetching {currency_id} currency logos: \"\n                                      f\"status: {resp.status} text: {await resp.text()}\")\n        # can't fetch image for some reason, use default\n        data_provider.set_currency_logo_url(currency_id, logo, dump=False)\n\n\nasync def _fetch_missing_currency_logos(data_provider, currency_ids):\n    # always use certify_aiohttp_client_session to avoid triggering rate limit with test request\n    async with aiohttp_util.certify_aiohttp_client_session() as session:\n        await asyncio.gather(\n            *(\n                _fetch_currency_logo(session, data_provider, currency_id)\n                for currency_id in currency_ids\n                if data_provider.get_currency_logo_url(currency_id) is None\n            )\n        )\n    data_provider.dump_saved_data()\n\n\ndef get_currency_logo_urls(currency_ids):\n    import tentacles.Services.Interfaces.web_interface.flask_util as flask_util\n    data_provider = flask_util.BrowsingDataProvider.instance()\n    if any(\n        data_provider.get_currency_logo_url(currency_id) is None\n        for currency_id in currency_ids\n    ):\n        interfaces_util.run_in_bot_async_executor(_fetch_missing_currency_logos(data_provider, currency_ids))\n    return [\n        {\n            \"id\": currency_id,\n            \"logo\": data_provider.get_currency_logo_url(currency_id)\n        }\n        for currency_id in currency_ids\n    ]\n\n\ndef get_traded_time_frames(exchange_manager, strategies=None, tentacles_setup_config=None) -> list:\n    if strategies is None:\n        return trading_api.get_relevant_time_frames(exchange_manager)\n    strategies_time_frames = []\n    for strategy_class in strategies:\n        strategies_time_frames += [\n            tf.value\n            for tf in get_strategy_required_time_frames(strategy_class, tentacles_setup_config)\n        ]\n    return [\n        commons_enums.TimeFrames(time_frame)\n        for time_frame in trading_api.get_all_exchange_time_frames(exchange_manager)\n        if time_frame in strategies_time_frames\n    ]\n\n\ndef get_or_init_FULL_EXCHANGE_LIST():\n    global _FULL_EXCHANGE_LIST\n    if _FULL_EXCHANGE_LIST is None:\n        _FULL_EXCHANGE_LIST = [\n            exchange\n            for exchange in set(ccxt.async_support.exchanges)\n            if exchange not in REMOVED_CCXT_EXCHANGES\n        ]\n    return _FULL_EXCHANGE_LIST\n\n\ndef auto_filled_exchanges(tentacles_setup_config=None):\n    global AUTO_FILLED_EXCHANGES\n    if AUTO_FILLED_EXCHANGES is None:\n        tentacles_setup_config = tentacles_setup_config or interfaces_util.get_edited_tentacles_config()\n        full_exchange_list = get_or_init_FULL_EXCHANGE_LIST()\n        AUTO_FILLED_EXCHANGES = [\n            exchange_name\n            for exchange_name in trading_api.get_auto_filled_exchange_names(tentacles_setup_config)\n            if exchange_name not in full_exchange_list\n        ]\n        full_exchange_list.extend(AUTO_FILLED_EXCHANGES)\n    return AUTO_FILLED_EXCHANGES\n\n\ndef get_full_exchange_list(tentacles_setup_config=None):\n    auto_filled_exchanges(tentacles_setup_config)\n    return get_or_init_FULL_EXCHANGE_LIST()\n\n\ndef get_full_configurable_exchange_list(remove_config_exchanges=False):\n    g_config = interfaces_util.get_global_config()\n    full_exchange_list = get_or_init_FULL_EXCHANGE_LIST()\n    if remove_config_exchanges:\n        user_exchanges = [e for e in g_config[commons_constants.CONFIG_EXCHANGES]]\n        full_exchange_list = list(set(full_exchange_list) - set(user_exchanges))\n    else:\n        full_exchange_list = full_exchange_list\n    # can't handle exchanges containing UPDATED_CONFIG_SEPARATOR character in their name\n    return [\n        exchange\n        for exchange in full_exchange_list\n        if constants.UPDATED_CONFIG_SEPARATOR not in exchange\n    ]\n\n\ndef get_default_exchange():\n    return ccxt.async_support.binance.__name__\n\n\ndef get_tested_exchange_list():\n    return [\n        exchange\n        for exchange in trading_constants.TESTED_EXCHANGES\n        if exchange in get_full_exchange_list()\n    ]\n\n\ndef get_simulated_exchange_list():\n    return [\n        exchange\n        for exchange in trading_constants.SIMULATOR_TESTED_EXCHANGES\n        if exchange in get_full_exchange_list()\n    ]\n\n\ndef get_other_exchange_list(remove_config_exchanges=False):\n    full_list = get_full_configurable_exchange_list(remove_config_exchanges)\n    return [\n        exchange\n        for exchange in full_list\n        if exchange not in trading_constants.TESTED_EXCHANGES and\n           exchange not in trading_constants.SIMULATOR_TESTED_EXCHANGES\n    ]\n\n\ndef get_enabled_exchange_types(config_exchanges):\n    return {\n        config.get(commons_constants.CONFIG_EXCHANGE_TYPE, trading_enums.ExchangeTypes.SPOT.value)\n        for config in config_exchanges.values()\n        if config.get(commons_constants.CONFIG_ENABLED_OPTION, True)\n    }\n\n\ndef get_exchanges_details(exchanges_config) -> dict:\n    details = {}\n    tentacles_setup_config = interfaces_util.get_edited_tentacles_config()\n    import tentacles.Trading.Exchange as exchanges\n    for exchange_name in exchanges_config:\n        exchange_class = tentacles_management.get_class_from_string(\n            exchange_name, trading_exchanges.AbstractExchange,\n            exchanges,\n            tentacles_management.default_parents_inspection\n        )\n        details[exchange_name] = {\n            \"has_websockets\": trading_api.supports_websockets(exchange_name, tentacles_setup_config),\n            \"configurable\": False if exchange_class is None else exchange_class.is_configurable(),\n            \"supported_exchange_types\": trading_api.get_supported_exchange_types(\n                exchange_name, tentacles_setup_config\n            ),\n            \"default_exchange_type\": trading_api.get_default_exchange_type(exchange_name),\n        }\n    return details\n\n\ndef get_compatibility_result(exchange_name, auth_success, compatible_account, supporter_account,\n                             configured_account, supporting_exchange, error_message, exchange_type):\n    return {\n        \"exchange\": exchange_name,\n        \"auth_success\": auth_success,\n        \"compatible_account\": compatible_account,\n        \"supporter_account\": supporter_account,\n        \"configured_account\": configured_account,\n        \"supporting_exchange\": supporting_exchange,\n        \"exchange_type\": exchange_type,\n        \"error_message\": error_message\n    }\n\n\nasync def _check_account_with_other_exchange_type_if_possible(\n    exchange_name: str, checked_config: dict, tentacles_setup_config, is_sandboxed: bool, supported_types: list\n):\n    is_compatible = False\n    auth_success = False\n    error = \"\"\n    ignored_type = checked_config.get(commons_constants.CONFIG_EXCHANGE_TYPE, commons_constants.DEFAULT_EXCHANGE_TYPE)\n    for supported_type in supported_types:\n        if supported_type.value == ignored_type:\n            continue\n        checked_config[commons_constants.CONFIG_EXCHANGE_TYPE] = supported_type.value\n        is_compatible, auth_success, error = await trading_api.is_compatible_account(\n            exchange_name,\n            checked_config,\n            tentacles_setup_config,\n            checked_config.get(commons_constants.CONFIG_EXCHANGE_SANDBOXED, False)\n        )\n        if auth_success:\n            return is_compatible, auth_success, error\n    # failed auth\n    return is_compatible, auth_success, error,\n\n\nasync def _fetch_is_compatible_account(exchange_name, to_check_config,\n                                       compatibility_results, is_sponsoring, is_supporter):\n    try:\n        checked_config = copy.deepcopy(to_check_config)\n        tentacles_setup_config = interfaces_util.get_edited_tentacles_config()\n        is_compatible, auth_success, error = await trading_api.is_compatible_account(\n            exchange_name,\n            checked_config,\n            tentacles_setup_config,\n            checked_config.get(commons_constants.CONFIG_EXCHANGE_SANDBOXED, False)\n        )\n        if not auth_success:\n            supported_types = trading_api.get_supported_exchange_types(exchange_name, tentacles_setup_config)\n            if len(supported_types) > 1:\n                is_compatible, auth_success, error = await _check_account_with_other_exchange_type_if_possible(\n                    exchange_name,\n                    checked_config,\n                    interfaces_util.get_edited_tentacles_config(),\n                    checked_config.get(commons_constants.CONFIG_EXCHANGE_SANDBOXED, False),\n                    supported_types\n                )\n        compatibility_results[exchange_name] = get_compatibility_result(\n            exchange_name,\n            auth_success,\n            is_compatible,\n            is_supporter,\n            True,\n            is_sponsoring,\n            error,\n            checked_config.get(commons_constants.CONFIG_EXCHANGE_TYPE, commons_constants.DEFAULT_EXCHANGE_TYPE)\n        )\n    except Exception as err:\n        bot_logging.get_logger(\"ConfigurationWebInterfaceModel\").exception(\n            err, True, f\"Error when checking {exchange_name} exchange credentials: {err}\"\n        )\n\n\n\ndef are_compatible_accounts(exchange_details: dict) -> dict:\n    compatibility_results = {}\n    check_coro = []\n    for exchange, exchange_detail in exchange_details.items():\n        exchange_name = exchange_detail[\"exchange\"]\n        api_key = exchange_detail[\"apiKey\"]\n        api_sec = exchange_detail[\"apiSecret\"]\n        api_pass = exchange_detail[\"apiPassword\"]\n        sandboxed = exchange_detail[commons_constants.CONFIG_EXCHANGE_SANDBOXED]\n        to_check_config = copy.deepcopy(interfaces_util.get_edited_config()[commons_constants.CONFIG_EXCHANGES].get(\n            exchange_name, {}))\n        if _is_real_exchange_value(api_key):\n            to_check_config[commons_constants.CONFIG_EXCHANGE_KEY] = configuration.encrypt(api_key).decode()\n        if _is_real_exchange_value(api_sec):\n            to_check_config[commons_constants.CONFIG_EXCHANGE_SECRET] = configuration.encrypt(api_sec).decode()\n        if _is_real_exchange_value(api_pass):\n            to_check_config[commons_constants.CONFIG_EXCHANGE_PASSWORD] = configuration.encrypt(api_pass).decode()\n        to_check_config[commons_constants.CONFIG_EXCHANGE_SANDBOXED] = sandboxed\n        is_compatible = auth_success = is_configured = False\n        is_sponsoring = trading_api.is_sponsoring(exchange_name)\n        is_supporter = authentication.Authenticator.instance().user_account.supports.is_supporting()\n        error = None\n        if _is_possible_exchange_config(to_check_config):\n            check_coro.append(_fetch_is_compatible_account(exchange_name, to_check_config,\n                                                           compatibility_results, is_sponsoring, is_supporter))\n        else:\n            compatibility_results[exchange_name] = get_compatibility_result(\n                exchange_name,\n                auth_success,\n                is_compatible,\n                is_supporter,\n                is_configured,\n                is_sponsoring,\n                error,\n                to_check_config.get(\n                    commons_constants.CONFIG_EXCHANGE_TYPE,\n                    commons_constants.DEFAULT_EXCHANGE_TYPE\n                )\n            )\n    if check_coro:\n        async def gather_wrapper(coros):\n            await asyncio.gather(*coros)\n        interfaces_util.run_in_bot_async_executor(\n            gather_wrapper(check_coro)\n        )\n        # trigger garbage collector as ccxt exchange can be heavy in RAM (20MB+)\n        gc.collect()\n    return compatibility_results\n\n\ndef _is_possible_exchange_config(exchange_config):\n    valid_count = 0\n    for key, value in exchange_config.items():\n        if key in commons_constants.CONFIG_EXCHANGE_ENCRYPTED_VALUES and _is_real_exchange_value(value):\n            valid_count += 1\n    # require at least 2 data to consider a configuration possible\n    return valid_count >= 2\n\n\ndef _is_real_exchange_value(value):\n    placeholder_key = \"******\"\n    if placeholder_key in value:\n        return False\n    return value not in commons_constants.DEFAULT_CONFIG_VALUES\n\n\ndef get_current_exchange():\n    for exchange_manager in interfaces_util.get_exchange_managers():\n        return trading_api.get_exchange_name(exchange_manager)\n    else:\n        return DEFAULT_EXCHANGE\n\n\ndef get_sandbox_exchanges() -> list:\n    return [\n        trading_api.get_exchange_name(exchange_manager)\n        for exchange_manager in interfaces_util.get_exchange_managers()\n        if trading_api.get_exchange_manager_is_sandboxed(exchange_manager)\n    ]\n\n\ndef get_distribution() -> octobot_enums.OctoBotDistribution:\n    return configuration_manager.get_distribution(interfaces_util.get_edited_config())\n\n\ndef change_reference_market_on_config_currencies(old_base_currency: str, new_quote_currency: str) -> bool:\n    \"\"\"\n    Change the base currency from old to new for all configured pair\n    :return: bool, str\n    \"\"\"\n    success = True\n    message = \"Reference market changed for each pair using the old reference market\"\n    try:\n        config_currencies = format_config_symbols(interfaces_util.get_edited_config())\n        for currencies_config in config_currencies.values():\n            currencies_config[commons_constants.CONFIG_CRYPTO_PAIRS] = \\\n                list(set([\n                    _change_base(pair, new_quote_currency)\n                    for pair in currencies_config[commons_constants.CONFIG_CRYPTO_PAIRS]\n                ]))\n        interfaces_util.get_edited_config(dict_only=False).save()\n    except Exception as e:\n        message = f\"Error while changing reference market on currencies list: {e}\"\n        success = False\n        bot_logging.get_logger(\"ConfigurationWebInterfaceModel\").exception(e, False)\n    return success, message\n\n\ndef _change_base(pair, new_quote_currency):\n    parsed_symbol = commons_symbols.parse_symbol(pair)\n    parsed_symbol.quote = new_quote_currency\n    return parsed_symbol.merged_str_symbol()\n\n\ndef send_command_to_activated_tentacles(command, wait_for_processing=True):\n    trading_mode_name = get_config_activated_trading_mode().get_name()\n    evaluator_names = [\n        evaluator.get_name()\n        for evaluator in get_config_activated_evaluators()\n    ]\n    send_command_to_tentacles(command, [trading_mode_name] + evaluator_names, wait_for_processing=wait_for_processing)\n\n\ndef send_command_to_tentacles(command, tentacle_names: list, wait_for_processing=True):\n    for tentacle_name in tentacle_names:\n        interfaces_util.run_in_bot_main_loop(\n            services_api.send_user_command(\n                interfaces_util.get_bot_api().get_bot_id(),\n                tentacle_name,\n                command,\n                None,\n                wait_for_processing=wait_for_processing\n            )\n        )\n\n\ndef reload_scripts():\n    try:\n        send_command_to_activated_tentacles(commons_enums.UserCommands.RELOAD_SCRIPT.value)\n        return {\"success\": True}\n    except Exception as e:\n        _get_logger().exception(e, True, f\"Failed to reload scripts: {e}\")\n        raise\n\n\ndef reload_activated_tentacles_config():\n    try:\n        send_command_to_activated_tentacles(commons_enums.UserCommands.RELOAD_CONFIG.value)\n        return {\"success\": True}\n    except Exception as e:\n        _get_logger().exception(e, True, f\"Failed to reload configurations: {e}\")\n        raise\n\n\ndef reload_tentacle_config(tentacle_name):\n    try:\n        send_command_to_tentacles(commons_enums.UserCommands.RELOAD_CONFIG.value, [tentacle_name])\n        return {\"success\": True}\n    except Exception as e:\n        _get_logger().exception(e, True, f\"Failed to reload {tentacle_name} configuration: {e}\")\n        raise\n\n\ndef update_config_currencies(currencies: dict, replace: bool=False):\n    \"\"\"\n    Update the configured currencies dict\n    :param currencies: currencies dict\n    :param replace: replace the current list\n    :return: bool, str\n    \"\"\"\n    success = True\n    message = \"Currencies list updated\"\n    try:\n        config_currencies = interfaces_util.get_edited_config()[commons_constants.CONFIG_CRYPTO_CURRENCIES]\n        # prevent format issues\n        checked_currencies = {\n            currency: {\n                commons_constants.CONFIG_CRYPTO_PAIRS: values[commons_constants.CONFIG_CRYPTO_PAIRS],\n                commons_constants.CONFIG_ENABLED_OPTION: values.get(commons_constants.CONFIG_ENABLED_OPTION, True)\n            }\n            for currency, values in currencies.items()\n            if (\n                isinstance(values.get(commons_constants.CONFIG_ENABLED_OPTION, True), bool)\n                and commons_constants.CONFIG_CRYPTO_PAIRS in values\n                and isinstance(values[commons_constants.CONFIG_CRYPTO_PAIRS], list)\n                and all(isinstance(pair, str) for pair in commons_constants.CONFIG_CRYPTO_PAIRS)\n            )\n        }\n        interfaces_util.get_edited_config()[commons_constants.CONFIG_CRYPTO_CURRENCIES] = (\n            checked_currencies if replace\n            else configuration.merge_dictionaries_by_appending_keys(\n                config_currencies, checked_currencies, merge_sub_array=True\n            )\n        )\n        interfaces_util.get_edited_config(dict_only=False).save()\n    except Exception as e:\n        message = f\"Error while updating currencies list: {e}\"\n        success = False\n        bot_logging.get_logger(\"ConfigurationWebInterfaceModel\").exception(e, False)\n    return success, message\n\n\ndef get_config_required_candles_count(exchange_manager):\n    return trading_api.get_required_historical_candles_count(exchange_manager)\n\n\ndef get_live_trading_enabled_exchange_managers():\n    return [\n        exchange_manager\n        for exchange_manager in trading_api.get_exchange_managers_from_exchange_ids(trading_api.get_exchange_ids())\n        if not trading_api.get_is_backtesting(exchange_manager)\n        and trading_api.is_trader_existing_and_enabled(exchange_manager)\n    ]"
  },
  {
    "path": "Services/Interfaces/web_interface/models/dashboard.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport numpy as np\nimport math\n\nimport octobot_backtesting.api as backtesting_api\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_trading.api as trading_api\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport tentacles.Services.Interfaces.web_interface.models.interface_settings as interface_settings\nimport tentacles.Services.Interfaces.web_interface.enums as enums\nimport octobot_commons.timestamp_util as timestamp_util\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.symbols as commons_symbols\n\nGET_SYMBOL_SEPARATOR = \"|\"\nDISPLAY_CANCELLED_TRADES = False\n\n\ndef parse_get_symbol(get_symbol):\n    return get_symbol.replace(GET_SYMBOL_SEPARATOR, \"/\")\n\n\ndef get_value_from_dict_or_string(data):\n    if isinstance(data, dict):\n        return data[\"value\"]\n    else:\n        return data\n\n\ndef format_trades(dict_trade_history):\n    trade_time_key = \"time\"\n    trade_price_key = \"price\"\n    trade_description_key = \"trade_description\"\n    trade_order_side_key = \"order_side\"\n    trades = {\n        trade_time_key: [],\n        trade_price_key: [],\n        trade_description_key: [],\n        trade_order_side_key: []\n    }\n    if not dict_trade_history:\n        return trades\n    for dict_trade in dict_trade_history:\n        status = dict_trade.get(trading_enums.ExchangeConstantsOrderColumns.STATUS.value,\n                                trading_enums.OrderStatus.UNKNOWN.value)\n        trade_side = trading_enums.TradeOrderSide(dict_trade[trading_enums.ExchangeConstantsOrderColumns.SIDE.value])\n        trade_type = trading_api.parse_trade_type(dict_trade)\n        if trade_type in (trading_enums.TraderOrderType.UNSUPPORTED, trading_enums.TraderOrderType.UNKNOWN):\n            trade_type = trade_side\n        if status is not trading_enums.OrderStatus.CANCELED.value or DISPLAY_CANCELLED_TRADES:\n            trade_time = dict_trade[trading_enums.ExchangeConstantsOrderColumns.TIMESTAMP.value]\n            if trade_time > trading_constants.MINIMUM_VAL_TRADE_TIME:\n                trades[trade_time_key].append(\n                    timestamp_util.convert_timestamp_to_datetime(\n                        trade_time, time_format=\"%y-%m-%d %H:%M:%S\", local_timezone=True\n                    )\n                )\n                trades[trade_price_key].append(\n                    float(dict_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]))\n                trades[trade_description_key].append(\n                    f\"{trade_type.name.replace('_', ' ')}: \"\n                    f\"{dict_trade[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value]} \"\n                    f\"{dict_trade[trading_enums.ExchangeConstantsOrderColumns.QUANTITY_CURRENCY.value]} \"\n                    f\"at {dict_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]} \"\n                    f\"{dict_trade[trading_enums.ExchangeConstantsOrderColumns.MARKET.value]}\")\n                trades[trade_order_side_key].append(trade_side.value)\n\n    return trades\n\n\ndef format_orders(order, min_order_time):\n    time_key = \"time\"\n    price_key = \"price\"\n    description_key = \"description\"\n    order_side_key = \"order_side\"\n    formatted_orders = {\n        time_key: [],\n        price_key: [],\n        description_key: [],\n        order_side_key: []\n    }\n    for order in order:\n        if order.creation_time > trading_constants.MINIMUM_VAL_TRADE_TIME:\n            formatted_orders[time_key].append(\n                timestamp_util.convert_timestamp_to_datetime(\n                    max(min_order_time, order.creation_time),\n                    time_format=\"%y-%m-%d %H:%M:%S\", local_timezone=True\n                )\n            )\n            formatted_orders[price_key].append(float(order.origin_price))\n            formatted_orders[description_key].append(\n                f\"{order.order_type.name.replace('_', ' ')}: {order.origin_quantity} {order.quantity_currency} \"\n                f\"at {order.origin_price}\"\n            )\n            formatted_orders[order_side_key].append(order.side.value)\n    return formatted_orders\n\n\ndef _remove_invalid_chars(string):\n    return string.split(\"[\")[0]\n\n\ndef _get_candles_reply(exchange, exchange_id, symbol, time_frame):\n    return {\n        \"exchange_name\": _remove_invalid_chars(exchange),\n        \"exchange_id\": exchange_id,\n        \"symbol\": symbol,\n        \"time_frame\": time_frame.value\n    }\n\n\ndef _get_first_exchange_identifiers(exchange_name=None, trading_exchange_only=False):\n    for exchange_manager in interfaces_util.get_exchange_managers():\n        if trading_exchange_only and not trading_api.is_trader_existing_and_enabled(exchange_manager):\n            continue\n        name = trading_api.get_exchange_name(exchange_manager)\n        if exchange_name is None or name == exchange_name:\n            return exchange_manager, name, trading_api.get_exchange_manager_id(exchange_manager)\n    raise KeyError(\"No exchange to be found\")\n\n\ndef get_first_exchange_data(exchange_name=None, trading_exchange_only=False):\n    return _get_first_exchange_identifiers(exchange_name, trading_exchange_only=trading_exchange_only)\n\n\ndef get_watched_symbol_data(symbol):\n    symbol_object = commons_symbols.parse_symbol(parse_get_symbol(symbol))\n    try:\n        last_possibility = {}\n        for exchange_manager in interfaces_util.get_exchange_managers():\n            exchange_id = trading_api.get_exchange_manager_id(exchange_manager)\n            exchange_name = trading_api.get_exchange_name(exchange_manager)\n            last_possibility = _get_candles_reply(\n                exchange_name,\n                exchange_id,\n                symbol,\n                _get_default_time_frame(exchange_name, exchange_id)\n            )\n            if symbol_object in trading_api.get_trading_symbols(exchange_manager):\n                return last_possibility\n        # symbol has not been found in exchange, still return the last exchange\n        # in case it becomes available\n        return last_possibility\n    except KeyError:\n        return {}\n\n\ndef _get_default_time_frame(exchange_name, exchange_id):\n    available_time_frames = trading_api.get_watched_timeframes(\n        trading_api.get_exchange_manager_from_exchange_name_and_id(exchange_name, exchange_id)\n    )\n    display_time_frame = commons_enums.TimeFrames(interface_settings.get_display_timeframe())\n    if display_time_frame in available_time_frames:\n        return display_time_frame\n    return available_time_frames[0]\n\n\ndef _is_symbol_data_available(exchange_manager, symbol):\n    return symbol in trading_api.get_trading_pairs(exchange_manager)\n\n\ndef get_startup_messages():\n    return interfaces_util.get_bot_api().get_startup_messages()\n\n\ndef get_first_symbol_data():\n    try:\n        exchange, exchange_name, exchange_id = _get_first_exchange_identifiers()\n        symbol = trading_api.get_trading_pairs(exchange)[0]\n        time_frame = _get_default_time_frame(exchange_name, exchange_id)\n        return _get_candles_reply(exchange_name, exchange_id, symbol, time_frame)\n    except (KeyError, IndexError):\n        return {}\n\n\ndef _create_candles_data(exchange_manager, symbol, time_frame, historical_candles, kline,\n                         bot_api, list_arrays, in_backtesting, ignore_trades, ignore_orders):\n    candles_key = \"candles\"\n    trades_key = \"trades\"\n    orders_key = \"orders\"\n    symbol_key = \"symbol\"\n    simulated_key = \"simulated\"\n    exchange_id_key = \"exchange_id\"\n    result_dict = {\n        candles_key: {},\n        trades_key: {},\n        orders_key: {},\n        simulated_key: trading_api.is_trader_simulated(exchange_manager),\n        symbol_key: symbol,\n        exchange_id_key: trading_api.get_exchange_manager_id(exchange_manager),\n    }\n    try:\n        data = historical_candles\n\n        # add kline as the last (current) candle that is not yet in history\n        if math.nan not in kline and data[commons_enums.PriceIndexes.IND_PRICE_TIME.value][-1] != kline[\n            commons_enums.PriceIndexes.IND_PRICE_TIME.value]:\n            data[commons_enums.PriceIndexes.IND_PRICE_TIME.value] = np.append(\n                data[commons_enums.PriceIndexes.IND_PRICE_TIME.value],\n                kline[commons_enums.PriceIndexes.IND_PRICE_TIME.value])\n            data[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value] = np.append(\n                data[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value],\n                kline[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value])\n            data[commons_enums.PriceIndexes.IND_PRICE_LOW.value] = np.append(\n                data[commons_enums.PriceIndexes.IND_PRICE_LOW.value],\n                kline[commons_enums.PriceIndexes.IND_PRICE_LOW.value])\n            data[commons_enums.PriceIndexes.IND_PRICE_OPEN.value] = np.append(\n                data[commons_enums.PriceIndexes.IND_PRICE_OPEN.value],\n                kline[commons_enums.PriceIndexes.IND_PRICE_OPEN.value])\n            data[commons_enums.PriceIndexes.IND_PRICE_HIGH.value] = np.append(\n                data[commons_enums.PriceIndexes.IND_PRICE_HIGH.value],\n                kline[commons_enums.PriceIndexes.IND_PRICE_HIGH.value])\n            data[commons_enums.PriceIndexes.IND_PRICE_VOL.value] = np.append(\n                data[commons_enums.PriceIndexes.IND_PRICE_VOL.value],\n                kline[commons_enums.PriceIndexes.IND_PRICE_VOL.value])\n        data_x = timestamp_util.convert_timestamps_to_datetime(data[commons_enums.PriceIndexes.IND_PRICE_TIME.value],\n                                                               time_format=\"%y-%m-%d %H:%M:%S\",\n                                                               local_timezone=True)\n        if not ignore_trades:\n            # handle trades after the 1st displayed candle start time for dashboard\n            first_time_to_handle_in_board = data[commons_enums.PriceIndexes.IND_PRICE_TIME.value][0]\n            trades_history = []\n            if trading_api.is_trader_existing_and_enabled(exchange_manager):\n                trades_history += trading_api.get_trade_history(exchange_manager, None, symbol,\n                                                                first_time_to_handle_in_board, True)\n\n            result_dict[trades_key] = format_trades(trades_history)\n\n        if not ignore_orders:\n            if trading_api.is_trader_existing_and_enabled(exchange_manager):\n                result_dict[orders_key] = format_orders(\n                    trading_api.get_open_orders(exchange_manager, symbol=symbol),\n                    # align time for historical candles only\n                    data[commons_enums.PriceIndexes.IND_PRICE_TIME.value][0]\n                    if len(data[commons_enums.PriceIndexes.IND_PRICE_TIME.value]) > 2 else 0\n                )\n\n        if list_arrays:\n            result_dict[candles_key] = {\n                enums.PriceStrings.STR_PRICE_TIME.value: data_x,\n                enums.PriceStrings.STR_PRICE_CLOSE.value: data[\n                    commons_enums.PriceIndexes.IND_PRICE_CLOSE.value].tolist(),\n                enums.PriceStrings.STR_PRICE_LOW.value: data[commons_enums.PriceIndexes.IND_PRICE_LOW.value].tolist(),\n                enums.PriceStrings.STR_PRICE_OPEN.value: data[commons_enums.PriceIndexes.IND_PRICE_OPEN.value].tolist(),\n                enums.PriceStrings.STR_PRICE_HIGH.value: data[commons_enums.PriceIndexes.IND_PRICE_HIGH.value].tolist(),\n                enums.PriceStrings.STR_PRICE_VOL.value: data[commons_enums.PriceIndexes.IND_PRICE_VOL.value].tolist()\n            }\n        else:\n            result_dict[candles_key] = {\n                enums.PriceStrings.STR_PRICE_TIME.value: data_x,\n                enums.PriceStrings.STR_PRICE_CLOSE.value: data[commons_enums.PriceIndexes.IND_PRICE_CLOSE.value],\n                enums.PriceStrings.STR_PRICE_LOW.value: data[commons_enums.PriceIndexes.IND_PRICE_LOW.value],\n                enums.PriceStrings.STR_PRICE_OPEN.value: data[commons_enums.PriceIndexes.IND_PRICE_OPEN.value],\n                enums.PriceStrings.STR_PRICE_HIGH.value: data[commons_enums.PriceIndexes.IND_PRICE_HIGH.value]\n            }\n    except IndexError:\n        pass\n    return result_dict\n\n\ndef _ensure_time_frame(time_frame: str):\n    try:\n        commons_enums.TimeFrames(time_frame)\n        return time_frame\n    except ValueError:\n        # if timeframe is invalid, use display timefrmae\n        return interface_settings.get_display_timeframe()\n\n\ndef get_currency_price_graph_update(exchange_id, symbol, time_frame, list_arrays=True, backtesting=False,\n                                    minimal_candles=False, ignore_trades=False, ignore_orders=False):\n    bot_api = interfaces_util.get_bot_api()\n    parsed_symbol = commons_symbols.parse_symbol(parse_get_symbol(symbol))\n    in_backtesting = backtesting_api.is_backtesting_enabled(interfaces_util.get_global_config()) or backtesting\n    exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n    symbol_id = str(parsed_symbol)\n    if time_frame is not None:\n        try:\n            time_frame = _ensure_time_frame(time_frame)\n            symbol_data = trading_api.get_symbol_data(exchange_manager, symbol_id, allow_creation=False)\n            limit = 1 if minimal_candles else -1\n            historical_candles = trading_api.get_symbol_historical_candles(symbol_data, time_frame, limit=limit)\n            kline = [math.nan]\n            if trading_api.has_symbol_klines(symbol_data, time_frame):\n                kline = trading_api.get_symbol_klines(symbol_data, time_frame)\n            if historical_candles is not None:\n                return _create_candles_data(exchange_manager, symbol_id, time_frame, historical_candles,\n                                            kline, bot_api, list_arrays, in_backtesting, ignore_trades, ignore_orders)\n        except KeyError:\n            traded_pairs = trading_api.get_trading_pairs(exchange_manager)\n            if not traded_pairs or symbol_id in traded_pairs:\n                # not started yet\n                return None\n            else:\n                return {\"error\": f\"no data for {parsed_symbol}\"}\n    return None\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/distributions/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom tentacles.Services.Interfaces.web_interface.models.distributions import market_making\n\nfrom tentacles.Services.Interfaces.web_interface.models.distributions.market_making import (\n    save_market_making_configuration,\n    get_market_making_services,\n)\n\n__all__ = [\n    \"save_market_making_configuration\",\n    \"get_market_making_services\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/distributions/market_making/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom tentacles.Services.Interfaces.web_interface.models.distributions.market_making import configuration\n\nfrom tentacles.Services.Interfaces.web_interface.models.distributions.market_making.configuration import (\n    save_market_making_configuration,\n    get_market_making_services,\n)\n\n__all__ = [\n    \"save_market_making_configuration\",\n    \"get_market_making_services\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/distributions/market_making/configuration.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport copy\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as symbol_utils\nimport octobot_commons.dict_util as dict_util\nimport octobot_commons.logging as commons_logging\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_tentacles_manager.api\n\nimport tentacles.Services.Interfaces.web_interface.models.json_schemas as json_schemas\nimport tentacles.Services.Interfaces.web_interface.models.configuration as models_configuration\nimport tentacles.Services.Interfaces.web_interface.models.trading as models_trading\nimport tentacles.Trading.Mode.market_making_trading_mode.market_making_trading as market_making_trading\n\n\n_LOGGER_NAME = \"MMConfigurationModel\"\n_MM_SERVICES = [\n    \"telegram\", \"web\"\n]\n\ndef save_market_making_configuration(\n    enabled_exchange: str,\n    trading_pair: typing.Optional[str],\n    exchange_configurations: list[dict],\n    trading_simulator_configuration: dict,\n    simulated_portfolio_configuration: list[dict],\n    trading_mode_name: str,\n    trading_mode_configuration: dict,\n) -> None:\n    _save_tentacle_config(trading_mode_name, trading_mode_configuration)\n    reference_exchange = trading_mode_configuration.get(\n        market_making_trading.MarketMakingTradingMode.REFERENCE_EXCHANGE\n    )\n    _save_user_config(\n        enabled_exchange, reference_exchange, trading_pair, exchange_configurations,\n        trading_simulator_configuration, simulated_portfolio_configuration,\n    )\n\n\ndef get_market_making_services() -> dict:\n    return {\n        name: service\n        for name, service in models_configuration.get_services_list().items()\n        if name in _MM_SERVICES\n    }\n\n\ndef _save_user_config(\n    enabled_exchange: typing.Optional[str],\n    reference_exchange: typing.Optional[str],\n    trading_pair: typing.Optional[str],\n    exchange_configurations: list[dict],\n    trading_simulator_configuration: dict,\n    simulated_portfolio_configuration: list[dict],\n) -> None:\n    current_edited_config = interfaces_util.get_edited_config(dict_only=False)\n\n    # exchanges: regenerate the whole configuration\n    exchange_config_update = json_schemas.json_exchange_config_to_config(\n        exchange_configurations, False\n    )\n    if exchange_config_update and enabled_exchange not in exchange_config_update:\n        # removed enabled exchange from exchange configs: use 1st exchange instead\n        enabled_exchange = next(iter(exchange_config_update))\n\n    for exchange in (enabled_exchange, reference_exchange):\n        if (\n            exchange\n            and exchange != market_making_trading.MarketMakingTradingMode.LOCAL_EXCHANGE_PRICE\n            and exchange in exchange_config_update\n        ):\n            # only enable selected exchange and reference exchanges, force spot trading\n            exchange_config_update[exchange].update({\n                commons_constants.CONFIG_ENABLED_OPTION: True,\n                commons_constants.CONFIG_EXCHANGE_TYPE: commons_constants.CONFIG_EXCHANGE_SPOT,\n            })\n    current_exchanges_config = copy.deepcopy(current_edited_config.config[commons_constants.CONFIG_EXCHANGES])\n    # nested_update_dict to keep nested key/val that might have been in previous config but are not in update\n    # don't pass current_exchanges_config directly to really delete exchanges\n    updated_exchange_config = {\n        exchange: exchange_config\n        for exchange, exchange_config in current_exchanges_config.items()\n        if exchange in exchange_config_update\n    }\n    dict_util.nested_update_dict(updated_exchange_config, exchange_config_update)\n\n    # currencies: regenerate the whole configuration\n    updated_currencies_config = {\n        trading_pair: {\n            commons_constants.CONFIG_ENABLED_OPTION: True,\n            commons_constants.CONFIG_CRYPTO_PAIRS: [trading_pair]\n        }\n    } if trading_pair else {}\n\n    # trader simulator\n    simulated_enabled = trading_simulator_configuration[commons_constants.CONFIG_ENABLED_OPTION]\n    updated_simulator_config = copy.deepcopy(current_edited_config.config[commons_constants.CONFIG_SIMULATOR])\n    previous_simulated_portfolio = copy.deepcopy(\n        updated_simulator_config.get(commons_constants.CONFIG_STARTING_PORTFOLIO)\n    )\n    simulator_config_update = {\n        **trading_simulator_configuration, **{\n            commons_constants.CONFIG_STARTING_PORTFOLIO: json_schemas.json_simulated_portfolio_to_config(\n                simulated_portfolio_configuration\n            )\n        }\n    }\n    updated_portfolio = simulator_config_update[commons_constants.CONFIG_STARTING_PORTFOLIO]\n    changed_portfolio = _filter_0_values(previous_simulated_portfolio) != _filter_0_values(updated_portfolio)\n    # replace portfolio to allow asset removal (otherwise nested_update_dict will never remove assets)\n    updated_simulator_config[commons_constants.CONFIG_STARTING_PORTFOLIO] = updated_portfolio\n    dict_util.nested_update_dict(updated_simulator_config, simulator_config_update)\n\n    # real trader\n    updated_trader_config = copy.deepcopy(current_edited_config.config[commons_constants.CONFIG_TRADER])\n    # only update the \"enabled\" state\n    updated_trader_config[commons_constants.CONFIG_ENABLED_OPTION] = not simulated_enabled\n\n    # trading\n    updated_trading_config = copy.deepcopy(current_edited_config.config[commons_constants.CONFIG_TRADING])\n    if trading_pair:\n        # only update the reference market\n        updated_trading_config[commons_constants.CONFIG_TRADER_REFERENCE_MARKET] = (\n            symbol_utils.parse_symbol(trading_pair).quote\n        )\n\n    update = {\n        commons_constants.CONFIG_CRYPTO_CURRENCIES: updated_currencies_config,\n        commons_constants.CONFIG_EXCHANGES: updated_exchange_config,\n        commons_constants.CONFIG_TRADING: updated_trading_config,\n        commons_constants.CONFIG_TRADER: updated_trader_config,\n        commons_constants.CONFIG_SIMULATOR: updated_simulator_config,\n    }\n    # apply & save changes\n    current_edited_config.config.update(update)\n    current_edited_config.save()\n    _get_logger().info(\n        f\"Configuration updated. Current profile: {current_edited_config.profile.name}\"\n    )\n    if changed_portfolio:\n        _get_logger().info(\"Simulated portfolio changed: resetting simulated portfolio content.\")\n        models_trading.clear_exchanges_portfolio_history(simulated_only=True)\n\n\ndef _filter_0_values(elements: dict) -> dict:\n    return {\n        key: val\n        for key, val in elements.items()\n        if val\n    }\n\n\ndef _save_tentacle_config(\n    trading_mode_name: str,\n    trading_mode_configuration: dict,\n) -> None:\n    tentacle_class = octobot_tentacles_manager.api.get_tentacle_class_from_string(trading_mode_name)\n    octobot_tentacles_manager.api.update_tentacle_config(\n        interfaces_util.get_edited_tentacles_config(),\n        tentacle_class,\n        trading_mode_configuration,\n        keep_existing=False,\n    )\n    _get_logger().info(\n        f\"{trading_mode_name} configuration updated.\"\n    )\n\n\ndef _get_logger():\n    return commons_logging.get_logger(_LOGGER_NAME)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/dsl.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.dsl_interpreter as dsl_interpreter\nimport octobot_services.interfaces.util as interfaces_util\n\nimport tentacles.Meta.DSL_operators.exchange_operators as dsl_operators\n\n\ndef get_dsl_keywords_docs() -> list[dsl_interpreter.OperatorDocs]:\n    exchange_managers = interfaces_util.get_exchange_managers()\n    all_operators = list(dsl_interpreter.get_all_operators()) # copy list to avoid modifying the original (cached) list\n    if exchange_managers:\n        # include exchange related operators\n        all_operators += dsl_operators.create_ohlcv_operators(\n            exchange_managers[0], None, None\n        )\n        all_operators += dsl_operators.create_portfolio_operators(\n            exchange_managers[0]\n        )\n    return [\n        operator.get_docs() \n        for operator in all_operators\n    ]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/interface_settings.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.constants as trading_constants\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.enums as web_enums\nimport tentacles.Services.Interfaces.web_interface.models.configuration as configuration_model\n\n\ndef get_watched_symbols():\n    config = get_web_interface_config()\n    try:\n        return config[web_interface.WebInterface.WATCHED_SYMBOLS]\n    except KeyError:\n        config[web_interface.WebInterface.WATCHED_SYMBOLS] = []\n    return config[web_interface.WebInterface.WATCHED_SYMBOLS]\n\n\ndef add_watched_symbol(symbol):\n    watched_symbols = get_watched_symbols()\n    if symbol not in watched_symbols:\n        watched_symbols.append(symbol)\n        return _save_edition()[0]\n    return True\n\n\ndef remove_watched_symbol(symbol):\n    watched_symbols = get_watched_symbols()\n    try:\n        watched_symbols.remove(symbol)\n        return _save_edition()[0]\n    except ValueError:\n        return True\n\n\ndef set_color_mode(color_mode: str):\n    try:\n        get_web_interface_config()[\n            web_interface.WebInterface.COLOR_MODE\n        ] = web_enums.ColorModes(color_mode).value\n    except ValueError:\n        return False, f\"invalid color mode: {color_mode}\"\n    return _save_edition()\n\n\ndef set_display_announcement(key: str, display: bool):\n    try:\n        get_web_interface_config()[\n            web_interface.WebInterface.ANNOUNCEMENTS\n        ][key] = display\n    except KeyError:\n        get_web_interface_config()[\n            web_interface.WebInterface.ANNOUNCEMENTS\n        ] = {key: display}\n    return _save_edition()\n\n\ndef get_display_announcement(key: str) -> bool:\n    try:\n        return get_web_interface_config()[\n            web_interface.WebInterface.ANNOUNCEMENTS\n        ][key]\n    except KeyError:\n        return True\n\n\ndef get_color_mode() -> web_enums.ColorModes:\n    return web_enums.ColorModes(get_web_interface_config().get(\n        web_interface.WebInterface.COLOR_MODE, web_enums.ColorModes.DEFAULT.value\n    ))\n\n\ndef get_display_timeframe():\n    return get_web_interface_config().get(\n        web_interface.WebInterface.DISPLAY_TIME_FRAME,\n        trading_constants.DISPLAY_TIME_FRAME.value\n    )\n\n\ndef get_display_orders():\n    return get_web_interface_config().get(web_interface.WebInterface.DISPLAY_ORDERS, True)\n\n\ndef set_display_timeframe(time_frame):\n    get_web_interface_config()[\n        web_interface.WebInterface.DISPLAY_TIME_FRAME\n    ] = time_frame\n    return _save_edition()\n\n\ndef set_display_orders(display_orders):\n    get_web_interface_config()[\n        web_interface.WebInterface.DISPLAY_ORDERS\n    ] = display_orders\n    return _save_edition()\n\n\ndef get_web_interface_config():\n    try:\n        return get_web_interface().local_config\n    except AttributeError:\n        return {}\n\n\ndef _save_edition():\n    success, message = configuration_model.update_tentacle_config(\n        web_interface.WebInterface.get_name(),\n        get_web_interface().local_config,\n        tentacle_class=web_interface.WebInterface\n    )\n    reload_config()\n    return success, message\n\n\ndef reload_config():\n    get_web_interface().reload_config()\n\n\ndef get_web_interface():\n    return interfaces_util.get_bot_api().get_interface(web_interface.WebInterface)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/json_schemas.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.configuration as configuration\n\n\nASSET = \"asset\"\nVALUE = \"value\"\nHIDDEN_VALUE = \"******\"\n\nNAME = \"name\"\nAPI_KEY = \"api-key\"\nAPI_SECRET = \"api-secret\"\nAPI_PASSWORD = \"api-password\"\n\n\nJSON_PORTFOLIO_SCHEMA = {\n    \"type\": \"array\",\n    \"uniqueItems\": True,\n    \"title\": \"Simulated portfolio\",\n    \"format\": \"table\",\n    \"items\": {\n        \"type\": \"object\",\n        \"title\": \"Asset\",\n        \"properties\": {\n            ASSET: {\n                \"title\": \"Asset\",\n                \"type\": \"string\",\n                \"enum\": [],\n            },\n            VALUE: {\n                \"title\": \"Holding\",\n                \"type\": \"number\",\n                \"minimum\": 0,\n            },\n        }\n    }\n}\n\n\ndef get_json_simulated_portfolio(user_config):\n    config_portfolio = user_config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO]\n    return [\n        {\n            ASSET: asset,\n            VALUE: value,\n        }\n        for asset, value in config_portfolio.items()\n    ]\n\n\ndef json_simulated_portfolio_to_config(json_portfolio_config: list[dict]) -> dict:\n    return {\n        entry[ASSET]: entry[VALUE]\n        for entry in json_portfolio_config\n    }\n\n\nJSON_TRADING_SIMULATOR_SCHEMA = {\n  \"type\": \"object\",\n  \"title\": \"Simulated trading configuration\",\n  \"additionalProperties\": False,\n  \"properties\": {\n    \"enabled\": {\n      \"title\": \"Enable trading simulator When checked, OctoBot will trade with the simulated portfolio\",\n      \"type\": \"boolean\",\n      \"format\": \"checkbox\",\n      \"options\": {\n        \"containerAttributes\": {\n          \"class\": \"mb-3\"\n        }\n      }\n    },\n    \"fees\": {\n      \"type\": \"object\",\n      \"additionalProperties\": False,\n      \"title\": \"Trading simulator fees\",\n      \"properties\": {\n        \"maker\": {\n          \"title\": \"Taker fees: maker trading fee as a % of the trade total cost.\",\n          \"type\": \"number\",\n          \"minimum\": -100,\n          \"maximum\": 100\n        },\n        \"taker\": {\n          \"title\": \"Taker fees: taker trading fee as a % of the trade total cost.\",\n          \"type\": \"number\",\n          \"minimum\": -100,\n          \"maximum\": 100,\n          \"step\": 0.01\n        }\n      }\n    }\n  }\n}\n\n\ndef get_json_trading_simulator_config(user_config: dict) -> dict:\n    return {\n        key: val\n        for key, val in user_config[commons_constants.CONFIG_SIMULATOR].items()\n        if key in (commons_constants.CONFIG_ENABLED_OPTION, commons_constants.CONFIG_SIMULATOR_FEES)\n    }\n\n\ndef get_json_exchanges_schema(exchanges: list[str]) -> dict:\n    return {\n        \"type\": \"array\",\n        \"uniqueItems\": True,\n        \"title\": \"Exchanges\",\n        \"format\": \"table\",\n        \"additionalProperties\": False,\n        \"items\": {\n            \"type\": \"object\",\n            \"id\": \"exchanges\",\n            \"title\": \"Exchange\",\n            \"additionalProperties\": False,\n            \"properties\": {\n                NAME: {\n                    \"title\": \"Name\",\n                    \"type\": \"string\",\n                    \"enum\": exchanges,\n                    \"propertyOrder\": 1,\n                },\n                API_KEY: {\n                    \"title\": \"API key: your API key for this exchange\",\n                    \"type\": \"string\",\n                    \"minLength\": 0,\n                    \"propertyOrder\": 2,\n                },\n                API_SECRET: {\n                    \"title\": \"API secret: your API secret for this exchange\",\n                    \"type\": \"string\",\n                    \"minLength\": 0,\n                    \"propertyOrder\": 3,\n                },\n                API_PASSWORD: {\n                    \"title\": \"API password: leave empty if not required by exchange\",\n                    \"type\": \"string\",\n                    \"minLength\": 0,\n                    \"propertyOrder\": 4,\n                },\n            }\n        }\n    }\n\n\ndef get_json_exchange_config(user_config: dict):\n    return [\n        {\n            NAME: name,\n            API_KEY: \"\" if configuration.has_invalid_default_config_value(values.get(commons_constants.CONFIG_EXCHANGE_KEY)) else HIDDEN_VALUE,\n            API_SECRET: \"\" if configuration.has_invalid_default_config_value(values.get(commons_constants.CONFIG_EXCHANGE_SECRET)) else HIDDEN_VALUE,\n            API_PASSWORD: \"\" if configuration.has_invalid_default_config_value(values.get(commons_constants.CONFIG_EXCHANGE_PASSWORD)) else HIDDEN_VALUE,\n        }\n        for name, values in user_config[commons_constants.CONFIG_EXCHANGES].items()\n    ]\n\ndef json_exchange_config_to_config(json_exchanges_config: list[dict], enabled: bool):\n    return {\n        config[NAME]: _get_exchange_config_from_json(config, enabled)\n        for config in json_exchanges_config\n    }\n\ndef _get_exchange_config_from_json(json_exchange_config: dict, enabled: bool) -> dict:\n    config = {\n        commons_constants.CONFIG_ENABLED_OPTION: enabled,\n    }\n    for json_key, config_key in (\n        (API_KEY, commons_constants.CONFIG_EXCHANGE_KEY),\n        (API_SECRET, commons_constants.CONFIG_EXCHANGE_SECRET),\n        (API_PASSWORD, commons_constants.CONFIG_EXCHANGE_PASSWORD),\n        (API_KEY, commons_constants.CONFIG_EXCHANGE_KEY),\n    ):\n        json_value = json_exchange_config[json_key]\n        if json_value != HIDDEN_VALUE:\n            # only add keys if their value is not HIDDEN_VALUE, use commons_constants.EMPTY_VALUE instead of \"\"\n            config[config_key] = json_value or commons_constants.NO_KEY_VALUE\n    return config\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/logs.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport shutil\nimport octobot.constants as constants\n\n\nLOG_EXPORT_FORMAT = \"zip\"\n\n\ndef export_logs(export_path):\n    shutil.make_archive(export_path, \"zip\", constants.LOGS_FOLDER)\n    return f\"{export_path}.{LOG_EXPORT_FORMAT}\"\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/medias.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_tentacles_manager.constants as tentacles_manager_constants\nimport octobot_commons.constants as commons_constants\n\nALLOWED_IMAGE_FORMATS = [\"png\", \"jpg\", \"jpeg\", \"gif\", \"svg\"]\nALLOWED_SOUNDS_FORMATS = [\"mp3\"]\n\n\ndef _is_valid_path(path, header):\n    return path.startswith(header) and \"..\" not in path\n\n\ndef is_valid_tentacle_image_path(path):\n    path_ending = path.split(\".\")[-1].lower()\n    return path_ending in ALLOWED_IMAGE_FORMATS and _is_valid_path(path, tentacles_manager_constants.TENTACLES_PATH)\n\n\ndef is_valid_profile_image_path(path):\n    path_ending = path.split(\".\")[-1].lower()\n    return path_ending in ALLOWED_IMAGE_FORMATS and _is_valid_path(path, commons_constants.USER_PROFILES_FOLDER)\n\n\ndef is_valid_audio_path(path):\n    path_ending = path.split(\".\")[-1].lower()\n    return path_ending in ALLOWED_SOUNDS_FORMATS and _is_valid_path(path, \"\")\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/profiles.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport os\n\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_commons.profiles as profiles\nimport octobot_commons.errors as errors\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.authentication as authentication\nimport octobot_trading.util as trading_util\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot.constants as constants\nimport octobot.community as community\nimport octobot.community.errors as community_errors\n\n\nACTIVATION = \"activation\"\nVERSION = \"version\"\nIMPORTED = \"imported\"\nREQUIRE_EXACT_VERSION = \"require_exact_version\"\nREAD_ERROR = \"read_error\"\n\n_PROFILE_TENTACLES_CONFIG_CACHE = {}\n\n\ndef get_current_profile():\n    return interfaces_util.get_edited_config(dict_only=False).profile\n\n\ndef duplicate_profile(profile_id):\n    to_duplicate = get_profile(profile_id)\n    new_profile = to_duplicate.duplicate(name=f\"{to_duplicate.name}_(copy)\", description=to_duplicate.description)\n    tentacles_manager_api.refresh_profile_tentacles_setup_config(new_profile.path)\n    interfaces_util.get_edited_config(dict_only=False).load_profiles()\n    return get_profile(new_profile.profile_id)\n\n\ndef convert_to_live_profile(profile_id):\n    profile = get_profile(profile_id)\n    profile.profile_type = commons_enums.ProfileType.LIVE\n    profile.validate_and_save_config()\n\n\ndef select_profile(profile_id):\n    _select_and_save(interfaces_util.get_edited_config(dict_only=False), profile_id)\n\n\ndef _select_and_save(config, profile_id):\n    config.select_profile(profile_id)\n    _update_edited_tentacles_config(config)\n    config.save()\n\n\ndef _update_edited_tentacles_config(config):\n    updated_tentacles_config = tentacles_manager_api.get_tentacles_setup_config(config.get_tentacles_config_path())\n    interfaces_util.set_edited_tentacles_config(updated_tentacles_config)\n\n\ndef get_profile(profile_id):\n    return interfaces_util.get_edited_config(dict_only=False).profile_by_id[profile_id]\n\n\ndef get_tentacles_setup_config_from_profile_id(profile_id):\n    return get_tentacles_setup_config_from_profile(get_profile(profile_id))\n\n\ndef get_tentacles_setup_config_from_profile(profile):\n    return tentacles_manager_api.get_tentacles_setup_config(\n        profile.get_tentacles_config_path()\n    )\n\n\ndef get_profiles(profile_type: commons_enums.ProfileType = None):\n    return {\n        identifier: profile\n        for identifier, profile in interfaces_util.get_edited_config(dict_only=False).profile_by_id.items()\n        if profile_type is None or profile.profile_type is profile_type\n    }\n\n\ndef _get_profile_setup_config(profile, reloading_profile):\n    if profile.profile_id == reloading_profile:\n        _PROFILE_TENTACLES_CONFIG_CACHE.pop(reloading_profile, None)\n        return tentacles_manager_api.get_tentacles_setup_config(\n            profile.get_tentacles_config_path()\n        )\n    try:\n        _PROFILE_TENTACLES_CONFIG_CACHE[profile.profile_id]\n    except KeyError:\n        _PROFILE_TENTACLES_CONFIG_CACHE[profile.profile_id] = \\\n            tentacles_manager_api.get_tentacles_setup_config(\n                profile.get_tentacles_config_path()\n            )\n    return _PROFILE_TENTACLES_CONFIG_CACHE[profile.profile_id]\n\n\ndef get_profiles_tentacles_details(profiles_list):\n    tentacles_by_profile_id = {}\n    current_profile_id = get_current_profile().profile_id\n    for profile in profiles_list.values():\n        try:\n            # force reload for current profile as tentacles setup config can change\n            tentacles_setup_config = _get_profile_setup_config(profile, current_profile_id)\n            tentacles_by_profile_id[profile.profile_id] = {\n                ACTIVATION: tentacles_manager_api.get_activated_tentacles(tentacles_setup_config),\n                VERSION: tentacles_manager_api.get_tentacles_installation_version(tentacles_setup_config),\n                IMPORTED: profile.imported,\n                REQUIRE_EXACT_VERSION: False,  # implement if exact version is required in profiles\n                READ_ERROR:\n                    not tentacles_manager_api.is_tentacles_setup_config_successfully_loaded(tentacles_setup_config),\n            }\n        except Exception:\n            # do not raise here to prevent avoid config display\n            pass\n    return tentacles_by_profile_id\n\n\ndef update_profile(profile_id, json_profile_desc, json_profile_content=None):\n    profile = get_profile(profile_id)\n    new_name = json_profile_desc.get(\"name\", profile.name)\n    renamed = profile.name != new_name\n    if renamed and get_current_profile().profile_id == profile_id:\n        return False, \"Can't rename the active profile\"\n    profile.name = new_name\n    profile.description = json_profile_desc.get(\"description\", profile.description)\n    profile.avatar = json_profile_desc.get(\"avatar\", profile.avatar)\n    profile.complexity = commons_enums.ProfileComplexity(int(json_profile_desc.get(\"complexity\", profile.complexity.value)))\n    profile.risk = commons_enums.ProfileRisk(int(json_profile_desc.get(\"risk\", profile.risk.value)))\n    if json_profile_content is not None:\n        profile.config = json_profile_content\n    profile.validate_and_save_config()\n    if renamed:\n        profile.rename_folder(new_name, False)\n    return True, \"Profile updated\"\n\n\ndef remove_profile(profile_id):\n    profile = None\n    if get_current_profile().profile_id == profile_id:\n        return profile, \"Can't remove the active profile\"\n    try:\n        profile = get_profile(profile_id)\n        interfaces_util.get_edited_config(dict_only=False).remove_profile(profile_id)\n    except errors.ProfileRemovalError as err:\n        return profile, err\n    return profile, None\n\n\ndef export_profile(profile_id, export_path) -> str:\n    return profiles.export_profile(get_profile(profile_id), export_path)\n\n\ndef import_profile(profile_path, name, profile_url=None):\n    profile = profiles.import_profile(profile_path, constants.PROFILE_FILE_SCHEMA, name=name, origin_url=profile_url)\n    interfaces_util.get_edited_config(dict_only=False).load_profiles()\n    return profile\n\n\ndef import_strategy_as_profile(authenticator, strategy: community.StrategyData, name: str, description: str):\n    if strategy.is_extension_only() and not authenticator.has_open_source_package():\n        raise community_errors.ExtensionRequiredError(\n            f\"The {constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME} is required to install this strategy\"\n        )\n    profile_data = interfaces_util.run_in_bot_main_loop(authenticator.get_strategy_profile_data(strategy.id))\n\n    profile = interfaces_util.run_in_bot_main_loop(\n        profiles.import_profile_data_as_profile(\n            profile_data,\n            constants.PROFILE_FILE_SCHEMA,\n            interfaces_util.get_bot_api().get_aiohttp_session(),\n            name=name,\n            description=description,\n            risk=strategy.get_risk(),\n            origin_url=strategy.get_product_url(),\n            logo_url=strategy.logo_url,\n            auto_update=strategy.is_auto_updated(),\n            force_simulator=True\n        )\n    )\n    interfaces_util.get_edited_config(dict_only=False).load_profiles()\n    return profile\n\n\ndef download_and_import_profile(profile_url):\n    name = profile_url.split('/')[-1]\n    if \"?\" in name:\n        # remove parameter\n        name = name.split(\"?\")[0]\n    file_path = profiles.download_profile(profile_url, name)\n    profile = import_profile(file_path, name, profile_url=profile_url)\n    if os.path.isfile(file_path):\n        os.remove(file_path)\n    return profile\n\n\ndef get_profile_name(profile_id) -> str:\n    return get_profile(profile_id).name\n\n\ndef get_forced_profile() -> profiles.Profile:\n    if constants.FORCED_PROFILE:\n        # env variables are priority 1\n        return get_current_profile()\n    try:\n        startup_info = interfaces_util.run_in_bot_main_loop(\n            authentication.Authenticator.instance().get_startup_info(),\n            log_exceptions=False\n        )\n        if startup_info.forced_profile_url:\n            return get_current_profile()\n    except community.BotError:\n        pass\n    return None\n\n\ndef is_real_trading(profile):\n    if trading_util.is_trader_enabled(profile.config):\n        return True\n    return False\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/strategy_optimizer.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport threading\n\nimport octobot.api as octobot_api\nimport octobot.constants as octobot_constants\nimport octobot_commons.logging as bot_logging\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_evaluators.evaluators as evaluators\nimport octobot_evaluators.api as evaluators_api\nimport octobot_services.interfaces.util as interfaces_util\nimport tentacles.Services.Interfaces.web_interface as web_interface_root\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Evaluator.Strategies as TentaclesStrategies\n\nLOGGER = bot_logging.get_logger(__name__)\n\n\ndef get_strategies_list(trading_mode):\n    try:\n        return trading_mode.get_required_strategies_names_and_count(interfaces_util.get_startup_tentacles_config())[0]\n    except Exception:\n        return []\n\n\ndef get_time_frames_list(strategy_name):\n    if strategy_name:\n        strategy_class = tentacles_management.get_class_from_string(strategy_name, evaluators.StrategyEvaluator,\n                                                                    TentaclesStrategies,\n                                                                    tentacles_management.evaluator_parent_inspection)\n        return [tf.value for tf in strategy_class.get_required_time_frames(\n            interfaces_util.get_global_config(),\n            interfaces_util.get_bot_api().get_tentacles_setup_config())]\n    else:\n        return []\n\n\ndef get_evaluators_list(strategy_name):\n    if strategy_name:\n        strategy_class = tentacles_management.get_class_from_string(strategy_name, evaluators.StrategyEvaluator,\n                                                                    TentaclesStrategies,\n                                                                    tentacles_management.evaluator_parent_inspection)\n        found_evaluators = evaluators_api.get_relevant_TAs_for_strategy(\n            strategy_class, interfaces_util.get_bot_api().get_tentacles_setup_config())\n        return set(evaluator.get_name() for evaluator in found_evaluators)\n    else:\n        return []\n\n\ndef get_risks_list():\n    return [i / 10 for i in range(10, 0, -1)]\n\n\ndef cancel_optimizer():\n    tools = web_interface_root.WebInterface.tools\n    optimizer = tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n    if optimizer is None:\n        return False, \"No optimizer is running\"\n    octobot_api.cancel_strategy_optimizer(optimizer)\n    return True, \"Optimizer is being cancelled\"\n\n\ndef start_optimizer(strategy, time_frames, evaluators, risks):\n    if not octobot_constants.ENABLE_BACKTESTING:\n        return False, \"Backtesting is disabled\"\n    try:\n        tools = web_interface_root.WebInterface.tools\n        optimizer = tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n        if optimizer is not None and octobot_api.is_optimizer_computing(optimizer):\n            return False, \"Optimizer already running\"\n        independent_backtesting = tools[constants.BOT_TOOLS_BACKTESTING]\n        if independent_backtesting and octobot_api.is_independent_backtesting_in_progress(independent_backtesting):\n            return False, \"A backtesting is already running\"\n        formatted_time_frames = time_frame_manager.parse_time_frames(time_frames)\n        float_risks = [float(risk) for risk in risks]\n        temp_independent_backtesting = octobot_api.create_independent_backtesting(\n            interfaces_util.get_global_config(), None, [])\n        optimizer_config = interfaces_util.run_in_bot_async_executor(\n            octobot_api.initialize_independent_backtesting_config(temp_independent_backtesting)\n        )\n        optimizer = octobot_api.create_strategy_optimizer(optimizer_config,\n                                                          interfaces_util.get_bot_api().get_edited_tentacles_config(),\n                                                          strategy)\n        tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER] = optimizer\n        thread = threading.Thread(target=octobot_api.find_optimal_configuration,\n                                  args=(optimizer, evaluators, formatted_time_frames, float_risks),\n                                  name=f\"{optimizer.get_name()}-WebInterface-runner\")\n        thread.start()\n        return True, \"Optimizer started\"\n    except Exception as e:\n        LOGGER.exception(e, True, f\"Error when starting optimizer: {e}\")\n        raise e\n\n\ndef get_optimizer_results():\n    optimizer = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n    if optimizer:\n        results = octobot_api.get_optimizer_results(optimizer)\n        return [result.get_result_dict(i) for i, result in enumerate(results)]\n    else:\n        return []\n\n\ndef get_optimizer_report():\n    if get_optimizer_status()[0] == \"finished\":\n        optimizer = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n        return octobot_api.get_optimizer_report(optimizer)\n    else:\n        return []\n\n\ndef get_current_run_params():\n    params = {\n        \"strategy_name\": [],\n        \"time_frames\": [],\n        \"evaluators\": [],\n        \"risks\": [],\n        \"trading_mode\": []\n    }\n    if web_interface_root.WebInterface.tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]:\n        optimizer = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n        params = {\n            \"strategy_name\": [octobot_api.get_optimizer_strategy(optimizer).get_name()],\n            \"time_frames\": [tf.value for tf in octobot_api.get_optimizer_all_time_frames(optimizer)],\n            \"evaluators\": octobot_api.get_optimizer_all_TAs(optimizer),\n            \"risks\": octobot_api.get_optimizer_all_risks(optimizer),\n            \"trading_mode\": [octobot_api.get_optimizer_trading_mode(optimizer)]\n        }\n    return params\n\n\ndef get_optimizer_status():\n    optimizer = web_interface_root.WebInterface.tools[constants.BOT_TOOLS_STRATEGY_OPTIMIZER]\n    if optimizer:\n        if octobot_api.is_optimizer_computing(optimizer):\n            overall_progress, remaining_time =\\\n                interfaces_util.run_in_bot_async_executor(octobot_api.get_optimizer_overall_progress(optimizer))\n            return \"computing\", octobot_api.get_optimizer_current_test_suite_progress(optimizer), \\\n                   overall_progress, remaining_time, \\\n                   octobot_api.get_optimizer_errors_description(optimizer)\n        else:\n            status = \"finished\" if octobot_api.is_optimizer_finished(optimizer) else \"starting\"\n            return status, 100, 100, 0, octobot_api.get_optimizer_errors_description(optimizer)\n    else:\n        return \"not started\", 0, 0, 0, None\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/tentacles.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot.constants as octobot_constants\nimport octobot.configuration_manager as configuration_manager\nimport octobot_commons.logging as bot_logging\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_tentacles_manager.constants as tentacles_manager_constants\n\nlogger = bot_logging.get_logger(\"TentaclesModel\")\n\n\ndef get_tentacles_packages():\n    return tentacles_manager_api.get_registered_tentacle_packages(\n        interfaces_util.get_bot_api().get_edited_tentacles_config())\n\n\ndef call_tentacle_manager(coro, *args, **kwargs):\n    return interfaces_util.run_in_bot_main_loop(coro(*args, **kwargs)) == 0\n\n\ndef _add_version_to_tentacles_package_path(path_or_url, version):\n    return f\"{path_or_url}/{version.replace('.', tentacles_manager_constants.ARTIFACT_VERSION_DOT_REPLACEMENT)}\"\n\n\ndef get_official_tentacles_url(use_beta_tentacles) -> str:\n    return configuration_manager.get_default_tentacles_url(\n        version=octobot_constants.BETA_TENTACLE_PACKAGE_NAME if use_beta_tentacles else None\n    )\n\n\ndef install_packages(path_or_url=None, version=None, authenticator=None):\n    message = \"Tentacles installed. Restart your OctoBot to load the new tentacles.\"\n    success = True\n    if path_or_url and version:\n        path_or_url = _add_version_to_tentacles_package_path(path_or_url, version)\n    for package_url in [path_or_url] if path_or_url else \\\n            tentacles_manager_api.get_registered_tentacle_packages(\n                interfaces_util.get_bot_api().get_edited_tentacles_config()).values():\n        if not package_url == tentacles_manager_constants.UNKNOWN_TENTACLES_PACKAGE_LOCATION:\n            if not call_tentacle_manager(tentacles_manager_api.install_all_tentacles,\n                                         package_url,\n                                         setup_config=interfaces_util.get_bot_api().get_edited_tentacles_config(),\n                                         aiohttp_session=interfaces_util.get_bot_api().get_aiohttp_session(),\n                                         bot_install_dir=octobot_constants.OCTOBOT_FOLDER,\n                                         authenticator=authenticator\n                                         ):\n                success = False\n        else:\n            message = \"Tentacles installed however it is impossible to re-install tentacles with unknown package origin\"\n    # reload profiles to display newly installed ones if any\n    interfaces_util.get_edited_config(dict_only=False).load_profiles()\n    if success:\n        return message\n    return False\n\n\ndef update_packages(authenticator=None):\n    message = \"Tentacles updated\"\n    success = True\n    for package_url in tentacles_manager_api.get_registered_tentacle_packages(\n            interfaces_util.get_bot_api().get_edited_tentacles_config()).values():\n        if package_url != tentacles_manager_constants.UNKNOWN_TENTACLES_PACKAGE_LOCATION:\n            if not call_tentacle_manager(tentacles_manager_api.update_all_tentacles,\n                                         package_url,\n                                         aiohttp_session=interfaces_util.get_bot_api().get_aiohttp_session(),\n                                         authenticator=authenticator):\n                success = False\n        else:\n            message = \"Tentacles updated however it is impossible to update tentacles with unknown package origin\"\n    if success:\n        return message\n    return False\n\n\ndef reset_packages():\n    if call_tentacle_manager(tentacles_manager_api.uninstall_all_tentacles,\n                             setup_config=interfaces_util.get_bot_api().get_edited_tentacles_config(),\n                             use_confirm_prompt=False):\n        return \"Reset successful\"\n    else:\n        return None\n\n\ndef update_modules(modules):\n    success = True\n    for url in [\n        get_official_tentacles_url(False),\n        # tentacles_manager_api.get_compiled_tentacles_url(\n        #     octobot_constants.DEFAULT_COMPILED_TENTACLES_URL,\n        #     octobot_constants.TENTACLES_REQUIRED_VERSION\n        # )\n    ]:\n        try:\n            call_tentacle_manager(tentacles_manager_api.update_tentacles,\n                                  modules,\n                                  url,\n                                  aiohttp_session=interfaces_util.get_bot_api().get_aiohttp_session(),\n                                  quite_mode=True)\n        except Exception:\n            success = False\n    if success:\n        return f\"{len(modules)} Tentacles updated\"\n    return None\n\n\ndef uninstall_modules(modules):\n    if call_tentacle_manager(tentacles_manager_api.uninstall_tentacles,\n                             modules):\n        return f\"{len(modules)} Tentacles uninstalled\"\n    else:\n        return None\n\n\ndef get_tentacles():\n    return tentacles_manager_api.get_installed_tentacles_modules()\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/trading.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport time\nimport sortedcontainers\n\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_trading.api as trading_api\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.logging as logging\nimport octobot_commons.timestamp_util as timestamp_util\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.pretty_printer as pretty_printer\nimport octobot_commons.symbols as commons_symbols\nimport tentacles.Services.Interfaces.web_interface.errors as errors\nimport tentacles.Services.Interfaces.web_interface.models.dashboard as dashboard\nimport tentacles.Services.Interfaces.web_interface.models.configuration as configuration\n\n\ndef ensure_valid_exchange_id(exchange_id) -> str:\n    try:\n        trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n    except KeyError as e:\n        raise errors.MissingExchangeId() from e\n\n\ndef get_exchange_watched_time_frames(exchange_id):\n    try:\n        exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n        return trading_api.get_watched_timeframes(exchange_manager), trading_api.get_exchange_name(exchange_manager)\n    except KeyError:\n        return [], \"\"\n\n\ndef get_all_watched_time_frames():\n    time_frames = []\n    for exchange_manager in trading_api.get_exchange_managers_from_exchange_ids(trading_api.get_exchange_ids()):\n        if not trading_api.get_is_backtesting(exchange_manager):\n            time_frames += trading_api.get_watched_timeframes(exchange_manager)\n    return time_frame_manager.sort_time_frames(list(set(time_frames)))\n\n\ndef get_initializing_currencies_prices_set(fetch_timeout):\n    initializing_currencies = set()\n    # if fetch_timeout is > 0 and prices are still missing after fetch_timeout, ignore them\n    if fetch_timeout and time.time() > interfaces_util.get_bot_api().get_start_time() + fetch_timeout:\n        return initializing_currencies\n    for exchange_manager in interfaces_util.get_exchange_managers():\n        initializing_currencies = initializing_currencies.union(\n            trading_api.get_initializing_currencies_prices(exchange_manager))\n    return initializing_currencies\n\n\ndef get_evaluation(symbol, exchange_name, exchange_id):\n    try:\n        if exchange_name:\n            exchange_manager = trading_api.get_exchange_manager_from_exchange_name_and_id(exchange_name, exchange_id)\n            for trading_mode in trading_api.get_trading_modes(exchange_manager):\n                if trading_api.get_trading_mode_symbol(trading_mode) == symbol:\n                    state_desc, val_state = trading_api.get_trading_mode_current_state(trading_mode)\n                    try:\n                        val_state = round(val_state)\n                    except TypeError:\n                        pass\n                    return f\"{state_desc.replace('_', ' ')}, {val_state}\"\n    except KeyError:\n        pass\n    return \"N/A\"\n\n\ndef get_exchanges_load():\n    return {\n        trading_api.get_exchange_name(exchange_manager): {\n            \"load\": trading_api.get_currently_handled_pair_with_time_frame(exchange_manager),\n            \"max_load\": trading_api.get_max_handled_pair_with_time_frame(exchange_manager),\n            \"overloaded\": trading_api.is_overloaded(exchange_manager),\n            \"has_websocket\": trading_api.get_has_websocket(exchange_manager),\n            \"has_reached_websocket_limit\": trading_api.get_has_reached_websocket_limit(exchange_manager)\n        }\n        for exchange_manager in interfaces_util.get_exchange_managers()\n    }\n\n\ndef _add_exchange_portfolio(portfolio, exchange, holdings_per_symbol):\n    exchanges_key = \"exchanges\"\n    total_key = \"total\"\n    free_key = \"free\"\n    locked_key = \"locked\"\n    for currency, amounts in portfolio.items():\n        total_amount = amounts.total\n        free_amount = amounts.available\n        if total_amount > 0:\n            if currency not in holdings_per_symbol:\n                holdings_per_symbol[currency] = {\n                    exchanges_key: {}\n                }\n            holdings_per_symbol[currency][exchanges_key][exchange] = {\n                total_key: total_amount,\n                free_key: free_amount,\n                locked_key: total_amount - free_amount,\n            }\n            holdings_per_symbol[currency][total_key] = holdings_per_symbol[currency].get(total_key, 0) + total_amount\n            holdings_per_symbol[currency][free_key] = holdings_per_symbol[currency].get(free_key, 0) + free_amount\n            holdings_per_symbol[currency][locked_key] = holdings_per_symbol[currency][total_key] - \\\n                holdings_per_symbol[currency][free_key]\n\n\ndef get_exchange_holdings_per_symbol():\n    holdings_per_symbol = {}\n    for exchange_manager in configuration.get_live_trading_enabled_exchange_managers():\n        portfolio = trading_api.get_portfolio(exchange_manager)\n        _add_exchange_portfolio(portfolio, trading_api.get_exchange_name(exchange_manager), holdings_per_symbol)\n    return holdings_per_symbol\n\n\ndef get_symbols_values(symbols, has_real_trader, has_simulated_trader):\n    loading = 0\n    value_per_symbols = {symbol: loading for symbol in symbols}\n    real_portfolio_holdings, simulated_portfolio_holdings = interfaces_util.get_portfolio_holdings()\n    portfolio = real_portfolio_holdings if has_real_trader else simulated_portfolio_holdings\n    value_per_symbols.update(portfolio)\n    return value_per_symbols\n\n\ndef _get_exchange_historical_portfolio(exchange_manager, currency, time_frame, from_timestamp, to_timestamp) -> list:\n    return [\n        {\n            trading_enums.HistoricalPortfolioValue.TIME.value: value[trading_enums.HistoricalPortfolioValue.TIME.value],\n            trading_enums.HistoricalPortfolioValue.VALUE.value: pretty_printer.get_min_string_from_number(\n                value[trading_enums.HistoricalPortfolioValue.VALUE.value]\n            )\n        }\n        for value in trading_api.get_portfolio_historical_values(\n            exchange_manager, currency, time_frame, from_timestamp=from_timestamp, to_timestamp=to_timestamp\n        )\n    ]\n\n\ndef _merge_all_exchanges_historical_portfolio(currency, time_frame, from_timestamp, to_timestamp):\n    merged_result = sortedcontainers.SortedDict()\n    for exchange_manager in configuration.get_live_trading_enabled_exchange_managers():\n        for value in _get_exchange_historical_portfolio(\n                exchange_manager, currency, time_frame, from_timestamp, to_timestamp\n        ):\n            if value[trading_enums.HistoricalPortfolioValue.TIME.value] not in merged_result:\n                merged_result[value[trading_enums.HistoricalPortfolioValue.TIME.value]] = \\\n                    value[trading_enums.HistoricalPortfolioValue.VALUE.value]\n            else:\n                merged_result[value[trading_enums.HistoricalPortfolioValue.TIME.value]] += str(decimal.Decimal(\n                    value[trading_enums.HistoricalPortfolioValue.VALUE.value]\n                ))\n    return [\n        {\n            trading_enums.HistoricalPortfolioValue.TIME.value: key,\n            trading_enums.HistoricalPortfolioValue.VALUE.value: val,\n        }\n        for key, val in merged_result.items()\n    ]\n\n\ndef get_portfolio_historical_values(currency, time_frame=None, from_timestamp=None, to_timestamp=None, exchange=None):\n    time_frame = commons_enums.TimeFrames(time_frame) if time_frame else commons_enums.TimeFrames.ONE_DAY\n    if exchange is None:\n        return _merge_all_exchanges_historical_portfolio(currency, time_frame, from_timestamp, to_timestamp)\n    return _get_exchange_historical_portfolio(\n        dashboard.get_first_exchange_data(exchange, trading_exchange_only=True)[0],\n        currency, time_frame, from_timestamp, to_timestamp\n    )\n\n\ndef _get_valid_pnl_history(exchange_manager, quote, symbol, since):\n    return [\n        pnl\n        for pnl in trading_api.get_completed_pnl_history(\n            exchange_manager,\n            quote=quote,\n            symbol=symbol,\n            since=since\n        )\n        if _is_valid_pnl(pnl)\n    ]\n\n\ndef _is_valid_pnl(pnl):\n    try:\n        return pnl.get_entry_time() and pnl.get_close_time()\n    except trading_errors.IncompletePNLError:\n        return False\n\n\ndef _get_pnl_history(exchange, quote, symbol, since):\n    if exchange:\n        return {\n            exchange: _get_valid_pnl_history(\n                dashboard.get_first_exchange_data(exchange, trading_exchange_only=True)[0],\n                quote, symbol, since\n            )\n        }\n    history = {}\n    for exchange_manager in configuration.get_live_trading_enabled_exchange_managers():\n        history[trading_api.get_exchange_name(exchange_manager)] = _get_valid_pnl_history(\n            exchange_manager, quote, symbol, since\n        )\n    return history\n\n\ndef get_pnl_history_symbols(exchange=None, quote=None, symbol=None, since=None):\n    return set(\n        historical_pnl.entries[0].symbol\n        for exchange_name, historical_pnl_elements in _get_pnl_history(exchange, quote, symbol, since).items()\n        for historical_pnl in historical_pnl_elements\n        if historical_pnl.entries\n    )\n\n\ndef _convert_timestamp(timestamp):\n    return timestamp_util.convert_timestamp_to_datetime(timestamp, time_format='%Y-%m-%d %H:%M:%S', local_timezone=True)\n\n\ndef get_pnl_history(exchange=None, quote=None, symbol=None, since=None, scale=None):\n    ENTRY_PRICE = \"en_p\"\n    EXIT_PRICE = \"ex_p\"\n    ENTRY_TIME = \"en_t\"\n    ENTRY_DATE = \"en_d\"\n    EXIT_TIME = \"ex_t\"\n    EXIT_DATE = \"ex_d\"\n    ENTRY_SIDE = \"en_s\"\n    EXIT_SIDE = \"ex_s\"\n    ENTRY_AMOUNT = \"en_a\"\n    EXIT_AMOUNT = \"ex_a\"\n    DETAILS = \"d\"\n    PNL = \"pnl\"\n    PNL_AMOUNT = \"pnl_a\"\n    EXCHANGE = \"ex\"\n    FEES = \"f\"\n    SPECIAL_FEES = \"s_f\"\n    BASE = \"b\"\n    QUOTE = \"q\"\n    CURRENCY = \"c\"\n    SYMBOL = \"s\"\n    TRADES_COUNT = \"tc\"\n    pnl_history = {}\n    use_detailed_history = not(scale)\n    scale_seconds = commons_enums.TimeFramesMinutes[commons_enums.TimeFrames(scale)] * \\\n        commons_constants.MINUTE_TO_SECONDS if scale else 1\n    symbol = symbol or None\n    # set quote filter to None when symbol is not provided\n    quote = None if symbol else quote\n    history_by_exchange = _get_pnl_history(exchange, quote, symbol, since)\n    invalid_pnls = 0\n    for exchange_name, historical_pnl_elements in history_by_exchange.items():\n        for historical_pnl in historical_pnl_elements:\n            try:\n                close_time = historical_pnl.get_close_time()\n                scaled_time = close_time - (close_time % scale_seconds)\n                pnl, pnl_p = historical_pnl.get_profits()\n                pnl_a = historical_pnl.get_closed_close_value()\n                if scaled_time not in pnl_history:\n                    pnl_history[scaled_time] = {\n                        PNL: pnl,\n                        PNL_AMOUNT: pnl_a,\n                        QUOTE: historical_pnl.entries[0].market,\n                        TRADES_COUNT: len(historical_pnl.entries) + len(historical_pnl.closes),\n                        DETAILS: None\n                    }\n                else:\n                    pnl_val = pnl_history[scaled_time]\n                    pnl_val[PNL] += pnl\n                    pnl_val[PNL_AMOUNT] += pnl_a\n                    pnl_val[TRADES_COUNT] += len(historical_pnl.entries) + len(historical_pnl.closes)\n                if use_detailed_history:\n                    pnl_history[scaled_time][DETAILS] = {\n                        ENTRY_TIME: historical_pnl.get_entry_time(),\n                        ENTRY_DATE: _convert_timestamp(historical_pnl.get_entry_time()),\n                        ENTRY_PRICE: float(historical_pnl.get_entry_price()),\n                        EXIT_PRICE: float(historical_pnl.get_close_price()),\n                        ENTRY_SIDE: historical_pnl.entries[0].side.value,\n                        EXIT_SIDE: historical_pnl.closes[0].side.value,\n                        ENTRY_AMOUNT: historical_pnl.get_total_entry_quantity(),\n                        EXIT_AMOUNT: historical_pnl.get_total_close_quantity(),\n                        SYMBOL: historical_pnl.entries[0].symbol,\n                        FEES: float(historical_pnl.get_paid_regular_fees_in_quote()),\n                        SPECIAL_FEES: [\n                            {\n                                CURRENCY: currency,\n                                FEES: float(value),\n                            }\n                            for currency, value in historical_pnl.get_paid_special_fees_by_currency().items()\n                        ],\n                        BASE: historical_pnl.entries[0].currency,\n                        EXCHANGE: exchange_name,\n                    }\n            except trading_errors.IncompletePNLError:\n                invalid_pnls += 1\n    if invalid_pnls:\n        logging.get_logger(\"TradingModel\").warning(f\"{invalid_pnls} invalid TradePNLs in history\")\n    return sorted(\n        [\n            {\n                EXIT_TIME: t,\n                EXIT_DATE: _convert_timestamp(t),\n                PNL: float(pnl[PNL]),\n                PNL_AMOUNT: float(pnl[PNL_AMOUNT]),\n                QUOTE: pnl[QUOTE],\n                TRADES_COUNT: pnl[TRADES_COUNT],\n                DETAILS: pnl[DETAILS],\n            }\n            for t, pnl in pnl_history.items()\n            # skip 0 value pnl in detailed history\n            if not use_detailed_history or (pnl[PNL] or pnl.get(DETAILS, {}).get(SPECIAL_FEES, 0))\n        ],\n        key=lambda x: x[EXIT_TIME]\n    )\n\n\ndef _get_dumped_data(real, simulated, dump_func):\n    return [\n        dumped\n        for dumped in tuple(\n            dump_func(order, False)\n            for order in real\n        ) + tuple(\n            dump_func(order, True)\n            for order in simulated\n        )\n        if dumped is not None\n    ]\n\n\nSYMBOL = \"symbol\"\nTYPE = \"type\"\nPRICE = \"price\"\nAMOUNT = \"amount\"\nEXCHANGE = \"exchange\"\nTIME = \"time\"\nDATE = \"date\"\nCOST = \"cost\"\nMARKET = \"market\"\nSIMULATED_OR_REAL = \"SoR\"\nID = \"id\"\nFEE_COST = \"fee_cost\"\nREF_MARKET_COST = \"ref_market_cost\"\nFEE_CURRENCY = \"fee_currency\"\nSIDE = \"side\"\nCONTRACT = \"contract\"\nVALUE = \"value\"\nENTRY_PRICE = \"entry_price\"\nLIQUIDATION_PRICE = \"liquidation_price\"\nMARGIN = \"margin\"\nUNREALIZED_PNL = \"unrealized_pnl\"\n\n\ndef _dump_order(order, is_simulated):\n    try:\n        market = _get_market(order.symbol)\n        return {\n            SYMBOL: order.symbol,\n            TYPE: order.order_type.name.replace(\"_\", \" \"),\n            PRICE: order.origin_price if not order.origin_stop_price else order.origin_stop_price,\n            AMOUNT: order.origin_quantity,\n            EXCHANGE: order.exchange_manager.exchange.name if order.exchange_manager else '',\n            DATE: _convert_timestamp(order.creation_time),\n            TIME: order.creation_time,\n            COST: _convert_amount(order.exchange_manager, order.total_cost, market) or order.total_cost,\n            MARKET: market,\n            SIMULATED_OR_REAL: (\n                \"Simulated\" if is_simulated\n                else \"Virtual\" if order.is_self_managed()\n                else \"Inactive\" if not order.is_active\n                else \"Real\"\n            ),\n            ID: order.order_id,\n        }\n    except Exception as err:\n        logging.get_logger(\"TradingModel\").exception(f\"Error when dumping order {err}, order: {order}\")\n        return None\n\n\ndef get_all_orders_data():\n    return _get_dumped_data(*interfaces_util.get_all_open_orders(), _dump_order)\n\n\ndef _convert_amount(exchange_manager, amount, currency):\n    multiplier = trading_api.get_currency_ref_market_value(exchange_manager, currency)\n    if multiplier is None:\n        return None\n    return float(multiplier * amount)\n\n\ndef _dump_trade(trade, is_simulated):\n    try:\n        market = _get_market(trade.symbol)\n        return {\n            SYMBOL: trade.symbol,\n            TYPE: trade.trade_type.name.replace(\"_\", \" \"),\n            PRICE: trade.executed_price,\n            AMOUNT: trade.executed_quantity,\n            EXCHANGE: trade.exchange_manager.exchange.name if trade.exchange_manager else '',\n            DATE: _convert_timestamp(trade.executed_time),\n            TIME: trade.executed_time,\n            COST: trade.total_cost,\n            REF_MARKET_COST: _convert_amount(trade.exchange_manager, trade.total_cost, market),\n            MARKET: market,\n            FEE_COST: trade.fee.get(trading_enums.FeePropertyColumns.COST.value, 0) if trade.fee else 0,\n            FEE_CURRENCY: trade.fee.get(trading_enums.FeePropertyColumns.CURRENCY.value, '') if trade.fee else '',\n            SIMULATED_OR_REAL: \"Simulated\" if is_simulated else \"Real\",\n            ID: trade.trade_id,\n        }\n    except Exception as err:\n        logging.get_logger(\"TradingModel\").exception(f\"Error when dumping trade {err}, trade: {trade}\")\n        return None\n\n\ndef get_all_trades_data(independent_backtesting=None):\n    return _get_dumped_data(*interfaces_util.get_trades_history(independent_backtesting=independent_backtesting), _dump_trade)\n\n\ndef _get_market(symbol_str):\n    symbol = commons_symbols.parse_symbol(symbol_str)\n    return symbol.settlement_asset or symbol.quote\n\n\ndef _dump_position(position, is_simulated):\n    try:\n        return {\n            SYMBOL: position.symbol,\n            SIDE: position.side.value,\n            CONTRACT: str(position.symbol_contract),\n            AMOUNT: position.size,\n            VALUE: position.value,\n            MARKET: position.currency if position.symbol_contract.is_inverse_contract() else position.market,\n            ENTRY_PRICE: position.entry_price,\n            LIQUIDATION_PRICE: position.liquidation_price,\n            MARGIN: position.margin,\n            UNREALIZED_PNL: position.unrealized_pnl,\n            EXCHANGE: position.exchange_manager.exchange.name if position.exchange_manager else '',\n            SIMULATED_OR_REAL: \"Simulated\" if is_simulated else \"Real\",\n        }\n    except Exception as err:\n        logging.get_logger(\"TradingModel\").exception(f\"Error when dumping position {err}, position: {position}\")\n        return None\n\n\ndef get_all_positions_data():\n    real, simulated = interfaces_util.get_all_positions()\n    return _get_dumped_data(\n        (position for position in real if not position.is_idle()),\n        (position for position in simulated if not position.is_idle()),\n        _dump_position\n    )\n\n\ndef clear_exchanges_orders_history(simulated_only=False):\n    _run_on_exchange_ids(trading_api.clear_orders_storage_history, simulated_only=simulated_only)\n    return {\"title\": \"Cleared orders history\"}\n\n\ndef clear_exchanges_trades_history(simulated_only=False):\n    _run_on_exchange_ids(trading_api.clear_trades_storage_history, simulated_only=simulated_only)\n    return {\"title\": \"Cleared trades history\"}\n\n\ndef clear_exchanges_transactions_history(simulated_only=False):\n    _run_on_exchange_ids(trading_api.clear_transactions_storage_history, simulated_only=simulated_only)\n    return {\"title\": \"Cleared transactions history\"}\n\n\ndef clear_exchanges_portfolio_history(simulated_only=False, simulated_portfolio=None):\n    # apply updated simulated portfolio to init new historical values on this new portfolio\n    simulated_portfolio = simulated_portfolio or \\\n        interfaces_util.get_edited_config(dict_only=True).get(commons_constants.CONFIG_SIMULATOR, {}).get(\n            commons_constants.CONFIG_STARTING_PORTFOLIO, None)\n    if simulated_portfolio:\n        _sync_run_on_exchange_ids(trading_api.set_simulated_portfolio_initial_config, simulated_only=simulated_only,\n                                  portfolio_content=simulated_portfolio)\n    _run_on_exchange_ids(trading_api.clear_portfolio_storage_history, simulated_only=simulated_only)\n    return {\"title\": \"Cleared portfolio history\"}\n\n\nasync def _async_run_on_exchange_ids(coro, exchange_ids, simulated_only, **kwargs):\n    for exchange_manager in trading_api.get_exchange_managers_from_exchange_ids(exchange_ids):\n        if (not simulated_only or trading_api.is_trader_simulated(exchange_manager)) \\\n                and not trading_api.get_is_backtesting(exchange_manager):\n            await coro(exchange_manager, **kwargs)\n\n\ndef _run_on_exchange_ids(coro, simulated_only=False, **kwargs):\n    interfaces_util.run_in_bot_main_loop(\n        _async_run_on_exchange_ids(coro, trading_api.get_exchange_ids(), simulated_only, **kwargs)\n    )\n\n\ndef _sync_run_on_exchange_ids(func, simulated_only=False, **kwargs):\n    for exchange_manager in trading_api.get_exchange_managers_from_exchange_ids(trading_api.get_exchange_ids()):\n        if (not simulated_only or trading_api.is_trader_simulated(exchange_manager)) \\\n                and not trading_api.get_is_backtesting(exchange_manager):\n            func(exchange_manager, **kwargs)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/models/web_interface_tab.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nclass WebInterfaceTab:\n    def __init__(\n        self, identifier, route, display_name, location, requires_open_source_package=False\n    ):\n        self.identifier = identifier\n        self.route = route\n        self.display_name = display_name\n        self.location = location\n        self.requires_open_source_package = requires_open_source_package\n\n    def is_available(self, has_open_source_package):\n        if not self.requires_open_source_package:\n            # is available in general\n            return True\n        if self.requires_open_source_package and has_open_source_package:\n            # is available if has_open_source_package\n            return True\n        # is not available\n        return False\n"
  },
  {
    "path": "Services/Interfaces/web_interface/plugins/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nfrom . import abstract_plugin\nfrom . import plugin_management\n\nfrom tentacles.Services.Interfaces.web_interface.plugins.abstract_plugin import (\n    AbstractWebInterfacePlugin,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.plugins.plugin_management import (\n    register_all_plugins,\n)\n\n__all__ = [\n    \"AbstractWebInterfacePlugin\",\n    \"register_all_plugins\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/plugins/abstract_plugin.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport flask\nimport os\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.logging as logging\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_tentacles_manager.api\nimport octobot_services.interfaces.util as interfaces_util\n\n\nclass AbstractWebInterfacePlugin(tentacles_management.AbstractTentacle):\n    USER_INPUT_TENTACLE_TYPE = commons_enums.UserInputTentacleTypes.WEB_PLUGIN\n    NAME = None\n    URL_PREFIX = None\n    PLUGIN_ROOT_FOLDER = None\n    TEMPLATE_FOLDER_NAME = \"templates\"\n    STATIC_FOLDER_NAME = \"static\"\n    ADDITIONAL_KWARGS = {}\n\n    def __init__(self, name, url_prefix, plugin_folder, template_folder, static_folder, **kwargs):\n        super().__init__()\n        self.name = name\n        self.url_prefix = url_prefix\n        self.plugin_folder = plugin_folder\n        self.template_folder = os.path.join(plugin_folder, template_folder) if plugin_folder else None\n        self.static_folder = os.path.join(plugin_folder, static_folder) if plugin_folder else None\n        self.kwargs = kwargs\n        self.blueprint = None\n        self.logger = logging.get_logger(self.name)\n\n    @classmethod\n    def get_name(cls):\n        return cls.__name__\n\n    def register_routes(self):\n        raise NotImplementedError(\"register_routes is not implemented\")\n\n    def get_tabs(self):\n        \"\"\"\n        Override if tabs are to be registered from this plugin\n        :return:\n        \"\"\"\n        return []\n\n    @classmethod\n    def init_user_inputs_from_class(cls, inputs: dict) -> None:\n        \"\"\"\n        Override if user inputs are required for this plugin\n        \"\"\"\n\n    @classmethod\n    def is_configurable(cls):\n        \"\"\"\n        Override if the tentacle is allowed to be configured\n        \"\"\"\n        return False\n\n    def blueprint_factory(self):\n        self.blueprint = flask.Blueprint(\n            self.name,\n            self.name,\n            url_prefix=self.url_prefix,\n            template_folder=self.template_folder,\n            static_folder=self.static_folder,\n            ** self.kwargs\n        )\n        return self.blueprint\n\n    @classmethod\n    def factory(cls, **kwargs):\n        if cls.NAME is None:\n            raise RuntimeError(f\"{cls.__name__}.NAME mush be set\")\n        return cls(\n            cls.NAME,\n            cls.URL_PREFIX or f\"/{cls.NAME}\",\n            cls.PLUGIN_ROOT_FOLDER,\n            cls.TEMPLATE_FOLDER_NAME,\n            cls.STATIC_FOLDER_NAME,\n            **{**cls.ADDITIONAL_KWARGS, **kwargs}\n        )\n\n    def register(self, server_instance):\n        self.blueprint_factory()\n        self.register_routes()\n        server_instance.register_blueprint(self.blueprint)\n        self.logger.debug(f\"Registered {self.name} plugin\")\n\n    @classmethod\n    def get_tentacle_config(cls, tentacles_setup_config=None):\n        return octobot_tentacles_manager.api.get_tentacle_config(\n            tentacles_setup_config or interfaces_util.get_edited_tentacles_config(), cls\n        )\n\n    def __str__(self):\n        return f\"name: {self.name} url_prefix: {self.url_prefix} \" \\\n               f\"template_folder: {self.template_folder} static_folder: {self.static_folder}\" \\\n               f\"kwargs: {self.kwargs}\"\n"
  },
  {
    "path": "Services/Interfaces/web_interface/plugins/plugin_management.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport os.path\n\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_commons.logging as logging\nimport tentacles.Services.Interfaces.web_interface.plugins as plugins\n\n\ndef register_all_plugins(server_instance, already_registered_plugins, **kwargs) -> list:\n    registered_plugins = []\n    already_registered_plugins_by_classes = {\n        plugin.__class__: plugin\n        for plugin in already_registered_plugins\n    }\n    for plugin_class in _get_all_plugins():\n        try:\n            can_use_plugin = True\n            # flask blueprints can't be be unregistered: reuse them when already registered\n            if plugin_class in already_registered_plugins_by_classes:\n                plugin = already_registered_plugins_by_classes[plugin_class]\n            else:\n                plugin = plugin_class.factory(**kwargs)\n                can_use_plugin = os.path.exists(plugin.plugin_folder)\n                if can_use_plugin:\n                    plugin.register(server_instance)\n            if can_use_plugin:\n                registered_plugins.append(plugin)\n        except Exception as e:\n            logging.get_logger(\"WebInterfacePluggingRegistration\").exception(\n                e,\n                True,\n                f\"Error when registering {plugin_class.__name__} plugin: {e}\"\n            )\n    return registered_plugins\n\n\ndef _get_all_plugins() -> list:\n    return tentacles_management.get_all_classes_from_parent(plugins.AbstractWebInterfacePlugin)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/security.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport datetime\nimport flask\nimport werkzeug.http as werk_http\nimport urllib.parse as url_parse\n\n\nCACHE_CONTROL_KEY = 'Cache-Control'\n\n\ndef register_responses_extra_header(flask_app, high_security_level):\n    # prepare extra response headers, see after_request\n    response_extra_headers = _prepare_response_extra_headers(high_security_level)\n\n    no_cache_headers = {\n        CACHE_CONTROL_KEY: 'no-cache, no-store, must-revalidate',\n        'Pragma': 'no-cache',\n        'Expires': '0',\n        'Last-Modified': werk_http.http_date(datetime.datetime.now()),\n    }\n\n    @flask_app.after_request\n    def after_request(response):\n        if CACHE_CONTROL_KEY not in response.headers:\n            response.headers.extend(no_cache_headers)\n        response.headers.extend(response_extra_headers)\n        return response\n\n\ndef _prepare_response_extra_headers(include_security_headers):\n    response_extra_headers = {\n        # uncomment to completely disable client caching (js and css files etc)\n        # 'Cache-Control': 'no-cache, no-store, must-revalidate',\n        # 'Pragma': 'no-cache',\n        # 'Expires': '0',\n        # 'Last-Modified': werk_http.http_date(datetime.now()),\n    }\n    if include_security_headers:\n        response_security_headers = {\n            # X-Frame-Options: page can only be shown in an iframe of the same site\n            'X-Frame-Options': 'SAMEORIGIN',\n            # ensure all app communication is sent over HTTPS\n            'Strict-Transport-Security': 'max-age=63072000; includeSubdomains',\n            # instructs the browser not to override the response content type\n            'X-Content-Type-Options': 'nosniff',\n            # enable browser cross-site scripting (XSS) filter\n            'X-XSS-Protection': '1; mode=block',\n        }\n        response_extra_headers.update(response_security_headers)\n\n    return response_extra_headers\n\n\ndef is_safe_url(target):\n    ref_url = url_parse.urlparse(flask.request.host_url)\n    test_url = url_parse.urlparse(url_parse.urljoin(flask.request.host_url, target))\n    return test_url.scheme in ('http', 'https') and \\\n        ref_url.netloc == test_url.netloc\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/css/bootstrap-editable.css",
    "content": "/*! X-editable - v1.5.1 \n* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery\n* http://github.com/vitalets/x-editable\n* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */\n.editableform {\n    margin-bottom: 0; /* overwrites bootstrap margin */\n}\n\n.editableform .control-group {\n    margin-bottom: 0; /* overwrites bootstrap margin */\n    white-space: nowrap; /* prevent wrapping buttons on new line */\n    line-height: 20px; /* overwriting bootstrap line-height. See #133 */\n}\n\n/* \n  BS3 width:1005 for inputs breaks editable form in popup \n  See: https://github.com/vitalets/x-editable/issues/393\n*/\n.editableform .form-control {\n    width: auto;\n}\n\n.editable-buttons {\n   display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */\n   vertical-align: top;\n   margin-left: 7px;\n   /* inline-block emulation for IE7*/\n   zoom: 1; \n   *display: inline;\n}\n\n.editable-buttons.editable-buttons-bottom {\n   display: block; \n   margin-top: 7px;\n   margin-left: 0;\n}\n\n.editable-input {\n    vertical-align: top; \n    display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */\n    width: auto; /* bootstrap-responsive has width: 100% that breakes layout */\n    white-space: normal; /* reset white-space decalred in parent*/\n   /* display-inline emulation for IE7*/\n   zoom: 1; \n   *display: inline;   \n}\n\n.editable-buttons .editable-cancel {\n   margin-left: 7px; \n}\n\n/*for jquery-ui buttons need set height to look more pretty*/\n.editable-buttons button.ui-button-icon-only {\n   height: 24px; \n   width: 30px;\n}\n\n.editableform-loading {\n    animation: spin 1s infinite linear;\n    -webkit-animation: spin2 1s infinite linear;\n    height: 25px;\n    width: auto; \n    min-width: 25px; \n}\n\n.editable-inline .editableform-loading {\n    background-position: left 5px;      \n}\n\n .editable-error-block {\n    max-width: 300px;\n    margin: 5px 0 0 0;\n    width: auto;\n    white-space: normal;\n}\n\n/*add padding for jquery ui*/\n.editable-error-block.ui-state-error {\n    padding: 3px;  \n}  \n\n.editable-error {\n   color: red;  \n}\n\n/* ---- For specific types ---- */\n\n.editableform .editable-date {\n    padding: 0; \n    margin: 0;\n    float: left;\n}\n\n/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */\n.editable-inline .add-on .icon-th {\n   margin-top: 3px;\n   margin-left: 1px; \n}\n\n\n/* checklist vertical alignment */\n.editable-checklist label input[type=\"checkbox\"], \n.editable-checklist label span {\n    vertical-align: middle;\n    margin: 0;\n}\n\n.editable-checklist label {\n    white-space: nowrap; \n}\n\n/* set exact width of textarea to fit buttons toolbar */\n.editable-wysihtml5 {\n    width: 566px; \n    height: 250px; \n}\n\n/* clear button shown as link in date inputs */\n.editable-clear {\n   clear: both;\n   font-size: 0.9em;\n   text-decoration: none;\n   text-align: right;\n}\n\n/* IOS-style clear button for text inputs */\n.editable-clear-x {\n   display: block;\n   width: 13px;    \n   height: 13px;\n   position: absolute;\n   opacity: 0.6;\n   z-index: 100;\n   \n   top: 50%;\n   right: 6px;\n   margin-top: -6px;\n   \n}\n\n.editable-clear-x:hover {\n   opacity: 1;\n}\n\n.editable-pre-wrapped {\n   white-space: pre-wrap;\n}\n.editable-container.editable-popup {\n    max-width: none !important; /* without this rule poshytip/tooltip does not stretch */\n}  \n\n.editable-container.popover {\n    width: auto; /* without this rule popover does not stretch */\n}\n\n.editable-container.editable-inline {\n    display: inline-block; \n    vertical-align: middle;\n    width: auto;\n    /* inline-block emulation for IE7*/\n    zoom: 1; \n    *display: inline;    \n}\n\n.editable-container.ui-widget {\n   font-size: inherit;  /* jqueryui widget font 1.1em too big, overwrite it */\n   z-index: 9990; /* should be less than select2 dropdown z-index to close dropdown first when click */\n}\n.editable-click, \na.editable-click, \na.editable-click:hover {\n    text-decoration: none;\n    border-bottom: dashed 1px #0088cc;\n}\n\n.editable-click.editable-disabled, \na.editable-click.editable-disabled, \na.editable-click.editable-disabled:hover {\n   color: #585858;  \n   cursor: default;\n   border-bottom: none;\n}\n\n.editable-empty, .editable-empty:hover, .editable-empty:focus{\n  font-style: italic; \n  color: #DD1144;  \n  /* border-bottom: none; */\n  text-decoration: none;\n}\n\n.editable-unsaved {\n  font-weight: bold; \n}\n\n.editable-unsaved:after {\n/*    content: '*'*/\n}\n\n.editable-bg-transition {\n  -webkit-transition: background-color 1400ms ease-out;\n  -moz-transition: background-color 1400ms ease-out;\n  -o-transition: background-color 1400ms ease-out;\n  -ms-transition: background-color 1400ms ease-out;\n  transition: background-color 1400ms ease-out;  \n}\n\n/*see https://github.com/vitalets/x-editable/issues/139 */\n.form-horizontal .editable\n{ \n    padding-top: 5px;\n    display:inline-block;\n}\n\n\n/*!\n * Datepicker for Bootstrap\n *\n * Copyright 2012 Stefan Petre\n * Improvements by Andrew Rowls\n * Licensed under the Apache License v2.0\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n */\n.datepicker {\n  padding: 4px;\n  -webkit-border-radius: 4px;\n  -moz-border-radius: 4px;\n  border-radius: 4px;\n  direction: ltr;\n  /*.dow {\n\t\tborder-top: 1px solid #ddd !important;\n\t}*/\n\n}\n.datepicker-inline {\n  width: 220px;\n}\n.datepicker.datepicker-rtl {\n  direction: rtl;\n}\n.datepicker.datepicker-rtl table tr td span {\n  float: right;\n}\n.datepicker-dropdown {\n  top: 0;\n  left: 0;\n}\n.datepicker-dropdown:before {\n  content: '';\n  display: inline-block;\n  border-left: 7px solid transparent;\n  border-right: 7px solid transparent;\n  border-bottom: 7px solid #ccc;\n  border-bottom-color: rgba(0, 0, 0, 0.2);\n  position: absolute;\n  top: -7px;\n  left: 6px;\n}\n.datepicker-dropdown:after {\n  content: '';\n  display: inline-block;\n  border-left: 6px solid transparent;\n  border-right: 6px solid transparent;\n  border-bottom: 6px solid #ffffff;\n  position: absolute;\n  top: -6px;\n  left: 7px;\n}\n.datepicker > div {\n  display: none;\n}\n.datepicker.days div.datepicker-days {\n  display: block;\n}\n.datepicker.months div.datepicker-months {\n  display: block;\n}\n.datepicker.years div.datepicker-years {\n  display: block;\n}\n.datepicker table {\n  margin: 0;\n}\n.datepicker td,\n.datepicker th {\n  text-align: center;\n  width: 20px;\n  height: 20px;\n  -webkit-border-radius: 4px;\n  -moz-border-radius: 4px;\n  border-radius: 4px;\n  border: none;\n}\n.table-striped .datepicker table tr td,\n.table-striped .datepicker table tr th {\n  background-color: transparent;\n}\n.datepicker table tr td.day:hover {\n  background: #eeeeee;\n  cursor: pointer;\n}\n.datepicker table tr td.old,\n.datepicker table tr td.new {\n  color: #999999;\n}\n.datepicker table tr td.disabled,\n.datepicker table tr td.disabled:hover {\n  background: none;\n  color: #999999;\n  cursor: default;\n}\n.datepicker table tr td.today,\n.datepicker table tr td.today:hover,\n.datepicker table tr td.today.disabled,\n.datepicker table tr td.today.disabled:hover {\n  background-color: #fde19a;\n  background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);\n  background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);\n  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));\n  background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);\n  background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);\n  background-image: linear-gradient(top, #fdd49a, #fdf59a);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);\n  border-color: #fdf59a #fdf59a #fbed50;\n  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);\n  color: #000;\n}\n.datepicker table tr td.today:hover,\n.datepicker table tr td.today:hover:hover,\n.datepicker table tr td.today.disabled:hover,\n.datepicker table tr td.today.disabled:hover:hover,\n.datepicker table tr td.today:active,\n.datepicker table tr td.today:hover:active,\n.datepicker table tr td.today.disabled:active,\n.datepicker table tr td.today.disabled:hover:active,\n.datepicker table tr td.today.active,\n.datepicker table tr td.today:hover.active,\n.datepicker table tr td.today.disabled.active,\n.datepicker table tr td.today.disabled:hover.active,\n.datepicker table tr td.today.disabled,\n.datepicker table tr td.today:hover.disabled,\n.datepicker table tr td.today.disabled.disabled,\n.datepicker table tr td.today.disabled:hover.disabled,\n.datepicker table tr td.today[disabled],\n.datepicker table tr td.today:hover[disabled],\n.datepicker table tr td.today.disabled[disabled],\n.datepicker table tr td.today.disabled:hover[disabled] {\n  background-color: #fdf59a;\n}\n.datepicker table tr td.today:active,\n.datepicker table tr td.today:hover:active,\n.datepicker table tr td.today.disabled:active,\n.datepicker table tr td.today.disabled:hover:active,\n.datepicker table tr td.today.active,\n.datepicker table tr td.today:hover.active,\n.datepicker table tr td.today.disabled.active,\n.datepicker table tr td.today.disabled:hover.active {\n  background-color: #fbf069 \\9;\n}\n.datepicker table tr td.today:hover:hover {\n  color: #000;\n}\n.datepicker table tr td.today.active:hover {\n  color: #fff;\n}\n.datepicker table tr td.range,\n.datepicker table tr td.range:hover,\n.datepicker table tr td.range.disabled,\n.datepicker table tr td.range.disabled:hover {\n  background: #eeeeee;\n  -webkit-border-radius: 0;\n  -moz-border-radius: 0;\n  border-radius: 0;\n}\n.datepicker table tr td.range.today,\n.datepicker table tr td.range.today:hover,\n.datepicker table tr td.range.today.disabled,\n.datepicker table tr td.range.today.disabled:hover {\n  background-color: #f3d17a;\n  background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);\n  background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);\n  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));\n  background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);\n  background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);\n  background-image: linear-gradient(top, #f3c17a, #f3e97a);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);\n  border-color: #f3e97a #f3e97a #edde34;\n  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);\n  -webkit-border-radius: 0;\n  -moz-border-radius: 0;\n  border-radius: 0;\n}\n.datepicker table tr td.range.today:hover,\n.datepicker table tr td.range.today:hover:hover,\n.datepicker table tr td.range.today.disabled:hover,\n.datepicker table tr td.range.today.disabled:hover:hover,\n.datepicker table tr td.range.today:active,\n.datepicker table tr td.range.today:hover:active,\n.datepicker table tr td.range.today.disabled:active,\n.datepicker table tr td.range.today.disabled:hover:active,\n.datepicker table tr td.range.today.active,\n.datepicker table tr td.range.today:hover.active,\n.datepicker table tr td.range.today.disabled.active,\n.datepicker table tr td.range.today.disabled:hover.active,\n.datepicker table tr td.range.today.disabled,\n.datepicker table tr td.range.today:hover.disabled,\n.datepicker table tr td.range.today.disabled.disabled,\n.datepicker table tr td.range.today.disabled:hover.disabled,\n.datepicker table tr td.range.today[disabled],\n.datepicker table tr td.range.today:hover[disabled],\n.datepicker table tr td.range.today.disabled[disabled],\n.datepicker table tr td.range.today.disabled:hover[disabled] {\n  background-color: #f3e97a;\n}\n.datepicker table tr td.range.today:active,\n.datepicker table tr td.range.today:hover:active,\n.datepicker table tr td.range.today.disabled:active,\n.datepicker table tr td.range.today.disabled:hover:active,\n.datepicker table tr td.range.today.active,\n.datepicker table tr td.range.today:hover.active,\n.datepicker table tr td.range.today.disabled.active,\n.datepicker table tr td.range.today.disabled:hover.active {\n  background-color: #efe24b \\9;\n}\n.datepicker table tr td.selected,\n.datepicker table tr td.selected:hover,\n.datepicker table tr td.selected.disabled,\n.datepicker table tr td.selected.disabled:hover {\n  background-color: #9e9e9e;\n  background-image: -moz-linear-gradient(top, #b3b3b3, #808080);\n  background-image: -ms-linear-gradient(top, #b3b3b3, #808080);\n  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));\n  background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);\n  background-image: -o-linear-gradient(top, #b3b3b3, #808080);\n  background-image: linear-gradient(top, #b3b3b3, #808080);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);\n  border-color: #808080 #808080 #595959;\n  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);\n  color: #fff;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.datepicker table tr td.selected:hover,\n.datepicker table tr td.selected:hover:hover,\n.datepicker table tr td.selected.disabled:hover,\n.datepicker table tr td.selected.disabled:hover:hover,\n.datepicker table tr td.selected:active,\n.datepicker table tr td.selected:hover:active,\n.datepicker table tr td.selected.disabled:active,\n.datepicker table tr td.selected.disabled:hover:active,\n.datepicker table tr td.selected.active,\n.datepicker table tr td.selected:hover.active,\n.datepicker table tr td.selected.disabled.active,\n.datepicker table tr td.selected.disabled:hover.active,\n.datepicker table tr td.selected.disabled,\n.datepicker table tr td.selected:hover.disabled,\n.datepicker table tr td.selected.disabled.disabled,\n.datepicker table tr td.selected.disabled:hover.disabled,\n.datepicker table tr td.selected[disabled],\n.datepicker table tr td.selected:hover[disabled],\n.datepicker table tr td.selected.disabled[disabled],\n.datepicker table tr td.selected.disabled:hover[disabled] {\n  background-color: #808080;\n}\n.datepicker table tr td.selected:active,\n.datepicker table tr td.selected:hover:active,\n.datepicker table tr td.selected.disabled:active,\n.datepicker table tr td.selected.disabled:hover:active,\n.datepicker table tr td.selected.active,\n.datepicker table tr td.selected:hover.active,\n.datepicker table tr td.selected.disabled.active,\n.datepicker table tr td.selected.disabled:hover.active {\n  background-color: #666666 \\9;\n}\n.datepicker table tr td.active,\n.datepicker table tr td.active:hover,\n.datepicker table tr td.active.disabled,\n.datepicker table tr td.active.disabled:hover {\n  background-color: #006dcc;\n  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);\n  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);\n  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));\n  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);\n  background-image: -o-linear-gradient(top, #0088cc, #0044cc);\n  background-image: linear-gradient(top, #0088cc, #0044cc);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);\n  border-color: #0044cc #0044cc #002a80;\n  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);\n  color: #fff;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.datepicker table tr td.active:hover,\n.datepicker table tr td.active:hover:hover,\n.datepicker table tr td.active.disabled:hover,\n.datepicker table tr td.active.disabled:hover:hover,\n.datepicker table tr td.active:active,\n.datepicker table tr td.active:hover:active,\n.datepicker table tr td.active.disabled:active,\n.datepicker table tr td.active.disabled:hover:active,\n.datepicker table tr td.active.active,\n.datepicker table tr td.active:hover.active,\n.datepicker table tr td.active.disabled.active,\n.datepicker table tr td.active.disabled:hover.active,\n.datepicker table tr td.active.disabled,\n.datepicker table tr td.active:hover.disabled,\n.datepicker table tr td.active.disabled.disabled,\n.datepicker table tr td.active.disabled:hover.disabled,\n.datepicker table tr td.active[disabled],\n.datepicker table tr td.active:hover[disabled],\n.datepicker table tr td.active.disabled[disabled],\n.datepicker table tr td.active.disabled:hover[disabled] {\n  background-color: #0044cc;\n}\n.datepicker table tr td.active:active,\n.datepicker table tr td.active:hover:active,\n.datepicker table tr td.active.disabled:active,\n.datepicker table tr td.active.disabled:hover:active,\n.datepicker table tr td.active.active,\n.datepicker table tr td.active:hover.active,\n.datepicker table tr td.active.disabled.active,\n.datepicker table tr td.active.disabled:hover.active {\n  background-color: #003399 \\9;\n}\n.datepicker table tr td span {\n  display: block;\n  width: 23%;\n  height: 54px;\n  line-height: 54px;\n  float: left;\n  margin: 1%;\n  cursor: pointer;\n  -webkit-border-radius: 4px;\n  -moz-border-radius: 4px;\n  border-radius: 4px;\n}\n.datepicker table tr td span:hover {\n  background: #eeeeee;\n}\n.datepicker table tr td span.disabled,\n.datepicker table tr td span.disabled:hover {\n  background: none;\n  color: #999999;\n  cursor: default;\n}\n.datepicker table tr td span.active,\n.datepicker table tr td span.active:hover,\n.datepicker table tr td span.active.disabled,\n.datepicker table tr td span.active.disabled:hover {\n  background-color: #006dcc;\n  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);\n  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);\n  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));\n  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);\n  background-image: -o-linear-gradient(top, #0088cc, #0044cc);\n  background-image: linear-gradient(top, #0088cc, #0044cc);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);\n  border-color: #0044cc #0044cc #002a80;\n  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);\n  color: #fff;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.datepicker table tr td span.active:hover,\n.datepicker table tr td span.active:hover:hover,\n.datepicker table tr td span.active.disabled:hover,\n.datepicker table tr td span.active.disabled:hover:hover,\n.datepicker table tr td span.active:active,\n.datepicker table tr td span.active:hover:active,\n.datepicker table tr td span.active.disabled:active,\n.datepicker table tr td span.active.disabled:hover:active,\n.datepicker table tr td span.active.active,\n.datepicker table tr td span.active:hover.active,\n.datepicker table tr td span.active.disabled.active,\n.datepicker table tr td span.active.disabled:hover.active,\n.datepicker table tr td span.active.disabled,\n.datepicker table tr td span.active:hover.disabled,\n.datepicker table tr td span.active.disabled.disabled,\n.datepicker table tr td span.active.disabled:hover.disabled,\n.datepicker table tr td span.active[disabled],\n.datepicker table tr td span.active:hover[disabled],\n.datepicker table tr td span.active.disabled[disabled],\n.datepicker table tr td span.active.disabled:hover[disabled] {\n  background-color: #0044cc;\n}\n.datepicker table tr td span.active:active,\n.datepicker table tr td span.active:hover:active,\n.datepicker table tr td span.active.disabled:active,\n.datepicker table tr td span.active.disabled:hover:active,\n.datepicker table tr td span.active.active,\n.datepicker table tr td span.active:hover.active,\n.datepicker table tr td span.active.disabled.active,\n.datepicker table tr td span.active.disabled:hover.active {\n  background-color: #003399 \\9;\n}\n.datepicker table tr td span.old,\n.datepicker table tr td span.new {\n  color: #999999;\n}\n.datepicker th.datepicker-switch {\n  width: 145px;\n}\n.datepicker thead tr:first-child th,\n.datepicker tfoot tr th {\n  cursor: pointer;\n}\n.datepicker thead tr:first-child th:hover,\n.datepicker tfoot tr th:hover {\n  background: #eeeeee;\n}\n.datepicker .cw {\n  font-size: 10px;\n  width: 12px;\n  padding: 0 2px 0 5px;\n  vertical-align: middle;\n}\n.datepicker thead tr:first-child th.cw {\n  cursor: default;\n  background-color: transparent;\n}\n.input-append.date .add-on i,\n.input-prepend.date .add-on i {\n  display: block;\n  cursor: pointer;\n  width: 16px;\n  height: 16px;\n}\n.input-daterange input {\n  text-align: center;\n}\n.input-daterange input:first-child {\n  -webkit-border-radius: 3px 0 0 3px;\n  -moz-border-radius: 3px 0 0 3px;\n  border-radius: 3px 0 0 3px;\n}\n.input-daterange input:last-child {\n  -webkit-border-radius: 0 3px 3px 0;\n  -moz-border-radius: 0 3px 3px 0;\n  border-radius: 0 3px 3px 0;\n}\n.input-daterange .add-on {\n  display: inline-block;\n  width: auto;\n  min-width: 16px;\n  height: 18px;\n  padding: 4px 5px;\n  font-weight: normal;\n  line-height: 18px;\n  text-align: center;\n  text-shadow: 0 1px 0 #ffffff;\n  vertical-align: middle;\n  background-color: #eeeeee;\n  border: 1px solid #ccc;\n  margin-left: -5px;\n  margin-right: -5px;\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/css/components/configuration.css",
    "content": "li.nav-item {\n    padding-right: 5px;\n}\n\n.navbar .nav-item .nav-link.active {\n    font-weight: 400;\n}\n\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/css/layout.css",
    "content": "/* Nav bar disabling */\na.disabled {\n  /* Make the disabled links grayish*/\n  color: gray;\n  /* And disable the pointer events */\n  pointer-events: none;\n}\n\n.icon-black {\n  color: black;\n}\n\n.danger-color{\n    background-color: #ff4444;\n}\n.danger-color-dark{\n    background-color: #CC0000;\n}\n.warning-color{\n    background-color: #ffbb33;\n}\n.warning-color-dark{\n    background-color: #FF8800;\n}\n.success-color{\n    background-color: #00C851;\n}\n.success-color-dark{\n    background-color: #007E33;\n}\n.info-color{\n    background-color: #33b5e5;\n}\n.info-color-dark{\n    background-color: #0099CC;\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/css/style.css",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n/* variables */\n/* commons */\n:root {\n    --mdb-body-font-family: DM Sans, sans-serif;\n    --local-secondary-bg-text-color: #0f1237;   /* same as dark mdb-primary */\n    --local-price-chart-sell-color: #F65A33;\n    --local-price-chart-stop-color: #FFA500;\n}\n\n:root[data-mdb-theme=light] {\n    --mdb-primary: #0f1237;\n    --mdb-primary-rgb: 15, 18, 55;\n    --mdb-primary-500: #5ba0cc;\n    --mdb-primary-500-rgb: 91, 106, 204;\n    --mdb-secondary: #85d6d7;\n    --mdb-secondary-rgb: 133, 214, 215;\n    --mdb-secondary-500: #65e7cf;\n    --mdb-secondary-500-rgb: 101, 231, 207;\n    --mdb-bg: #f3f6f8;\n    --mdb-bg-500: #85d6d7;\n\n    /* text */\n    --mdb-emphasis-color-rgb: var(--mdb-primary-rgb);\n    --mdb-primary-text-emphasis: var(--mdb-primary);\n    --mdb-heading-color: var(--mdb-primary);\n\n    /* links */\n    --mdb-link-color: var(--mdb-secondary);\n    --mdb-link-color-rgb: var(--mdb-secondary-rgb);\n    --mdb-link-hover-color: var(--mdb-secondary-500);\n    --mdb-link-hover-color-rgb: var(--mdb-secondary-500-rgb);\n\n    /* navbar */\n    --mdb-navbar-color: var(--mdb-primary);\n    --mdb-nav-link-color: var(--mdb-primary);\n\n    /* body */\n    --mdb-body-color: var(--mdb-primary);\n    --mdb-body-color-rgb: var(--mdb-primary-rgb);\n    --mdb-body-bg: var(--mdb-bg);\n    --mdb-body-bg-rgb: var(--mdb-bg-rgb);\n\n    /* button */\n    --mdb-button-color: var(--mdb-primary);\n    --mdb-outline-button-color: var(--mdb-primary);\n\n    /* table */\n    --mdb-table-headings-bg: var(--mdb-secondary);\n    --mdb-table-headings-color: var(--mdb-primary);\n\n    /* cards */\n    --mdb-card-modified-border-color: var(--mdb-orange);\n    --mdb-card-container-modified-border-color: var(--mdb-orange);\n    --mdb-card-status-color: var(--mdb-bg);\n    --mdb-card-very-long-border-color: var(--mdb-secondary-500);\n    --mdb-card-long-border-color: var(--mdb-secondary);\n    --mdb-card-short-border-color: var(--mdb-orange);\n    --mdb-card-very-short-border-color: var(--mdb-red);\n    --mdb-card-bg: var(--mdb-bg);\n    --mdb-card-color: var(--mdb-primary);\n    --mdb-surface-bg: var(--mdb-bg);\n\n    /* navbar */\n    --mdb-navbar-bg: var(--mdb-bg);\n    --mdb-navbar-brand-color: var(--mdb-primary);\n\n    /* local */\n    /* config cards */\n    --local-config-card--border-width: 0px;\n\n    /* price charts */\n    --local-price-chart-buy-color: #6cb596;\n    --local-price-chart-candle-sell-color: var(--local-price-chart-sell-color);\n    --local-price-chart-candle-buy-color: #6cb596;\n}\n\n:root[data-mdb-theme=dark] {\n    --mdb-primary: #f3f6f8;\n    --mdb-primary-rgb: 243, 246, 248;\n    --mdb-secondary: #85d6d7;\n    --mdb-secondary-rgb: 133, 214, 215;\n    --mdb-secondary-500: #65e7cf;\n    --mdb-secondary-500-rgb: 101, 231, 207;\n    --mdb-bg: #0f1237;\n    --mdb-bg-rgb: 15, 18, 55;\n    --mdb-bg-500: #19283e;\n\n    /* text */\n    --mdb-emphasis-color-rgb: var(--mdb-primary-rgb);\n    --mdb-primary-text-emphasis: var(--mdb-primary);\n    --mdb-heading-color: var(--mdb-primary);\n\n    /* links */\n    --mdb-link-color: var(--mdb-secondary);\n    --mdb-link-color-rgb: var(--mdb-secondary-rgb);\n    --mdb-link-hover-color: var(--mdb-secondary-500);\n    --mdb-link-hover-color-rgb: var(--mdb-secondary-500-rgb);\n\n    /* navbar */\n    --mdb-navbar-color: var(--mdb-primary);\n    --mdb-nav-link-color: var(--mdb-primary);\n\n    /* body */\n    --mdb-body-color: var(--mdb-primary);\n    --mdb-body-color-rgb: var(--mdb-primary-rgb);\n    --mdb-body-bg: var(--mdb-bg);\n    --mdb-body-bg-rgb: var(--mdb-bg-rgb);\n\n    /* button */\n    --mdb-button-color: var(--mdb-bg);\n    --mdb-outline-button-color: var(--mdb-primary);\n\n    /* table */\n    --mdb-table-headings-bg: var(--mdb-bg);\n    --mdb-table-headings-color: var(--mdb-secondary);\n\n   /* cards */\n    --mdb-card-modified-border-color: var(--mdb-orange);\n    --mdb-card-container-modified-border-color: var(--mdb-orange);\n    --mdb-card-status-color: var(--mdb-bg);\n    --mdb-card-very-long-border-color: var(--mdb-secondary-500);\n    --mdb-card-long-border-color: var(--mdb-secondary);\n    --mdb-card-short-border-color: var(--mdb-orange);\n    --mdb-card-very-short-border-color: var(--mdb-red);\n    --mdb-card-bg: var(--mdb-secondary);\n    --mdb-card-color: var(--mdb-primary);\n    --mdb-surface-bg: var(--mdb-bg-500);\n\n    /* navbar */\n    --mdb-navbar-bg: var(--mdb-bg);\n    --mdb-navbar-brand-color: var(--mdb-primary);\n\n    /* local */\n    /* config cards */\n    --local-config-card--border-width: 1px;\n\n    /* price charts */\n    --local-price-chart-buy-color: #6cb596;\n    --local-price-chart-candle-sell-color: var(--local-price-chart-sell-color);\n    --local-price-chart-candle-buy-color: #65e7cf;\n}\n\n/* Fix incompatibility with bootstrap 4 */\na:hover {\n    color: var(--mdb-link-hover-color);\n}\n\n/* Fix declarations */\n.nav-link, .navbar-brand, .card-body, .card-footer, .card-header, .select2-results__option, .filter-option {\n    color: var(--mdb-primary);\n}\n\n.dropdown, .datepicker, .select2-container--default .select2-selection--multiple, .select2-results__option {\n    background-color: var(--mdb-bg);\n}\n\n.dropdown.bootstrap-select, .datepicker, .select2-selection select2-selection--multiple {\n    border: solid black 1px;\n    border-radius: 4px;\n}\n\n.nav-tabs .nav-link {\n    --mdb-nav-tabs-link-active-color: var(--mdb-secondary);\n    --mdb-nav-tabs-link-active-border-color: var(--mdb-secondary);\n}\n\n.btn-primary {\n    --mdb-btn-bg: var(--mdb-secondary);\n    --mdb-btn-color: var(--mdb-button-color);\n    --mdb-btn-box-shadow: 0 4px 9px -4px var(--mdb-secondary-500);\n    --mdb-btn-hover-bg: var(--mdb-secondary-500);\n    --mdb-btn-focus-bg: var(--mdb-secondary-500);\n    --mdb-btn-active-bg: var(--mdb-secondary-500);\n    --mdb-btn-disabled-bg:  var(--mdb-secondary);\n}\n\n.btn-outline-primary {\n    /*--mdb-btn-bg: var(--mdb-secondary);*/\n    --mdb-btn-color: var(--mdb-outline-button-color) !important;\n    --mdb-btn-box-shadow: 0 4px 9px -4px var(--mdb-secondary);\n    --mdb-btn-hover-color: var(--mdb-primary);\n    --mdb-btn-hover-border-color: var(--mdb-secondary);\n    --mdb-btn-focus-shadow-rgb: var(--mdb-secondary-rgb);\n    --mdb-btn-hover-bg: var(--mdb-secondary);\n    --mdb-btn-focus-bg: var(--mdb-secondary);\n    --mdb-btn-active-bg: var(--mdb-secondary);\n    --mdb-btn-focus-color:  var(--mdb-secondary);\n    --mdb-btn-active-color: var(--mdb-secondary);\n    --mdb-btn-active-border-color: var(--mdb-secondary);\n    --mdb-btn-outline-border-color: var(--mdb-secondary);\n    --mdb-btn-outline-focus-border-color: var(--mdb-secondary);\n    --mdb-btn-outline-hover-border-color: var(--mdb-secondary);\n}\n\n.editable-cancel {\n    --mdb-btn-bg: var(--mdb-bg);\n    --mdb-btn-color: var(--mdb-primary);\n    --mdb-btn-box-shadow: 0 4px 9px -4px var(--mdb-secondary);\n    --mdb-btn-hover-color: var(--mdb-primary);\n    --mdb-btn-hover-border-color: var(--danger);\n    --mdb-btn-focus-shadow-rgb: var(--mdb-secondary-rgb);\n    --mdb-btn-hover-bg: var(--danger);\n    --mdb-btn-focus-bg: var(--danger);\n    --mdb-btn-active-bg: var(--danger);\n    --mdb-btn-focus-color:  var(--mdb-secondary);\n    --mdb-btn-active-color: var(--mdb-secondary);\n    --mdb-btn-active-border-color: var(--danger);\n    --mdb-btn-outline-border-color: var(--mdb-secondary);\n    --mdb-btn-outline-focus-border-color: var(--mdb-secondary);\n    --mdb-btn-outline-hover-border-color: var(--mdb-secondary);\n}\n\n.table th, th {\n    color: var(--mdb-table-headings-color);\n}\n\n.table {\n    --mdb-table-color: var(--mdb-primary);\n    --mdb-table-striped-color: var(--mdb-primary);\n}\n\n.table td, .table th {\n    border-top: none;\n}\n\n/* toast */\n#toast-container>.toast-warning {\n    background-image: none !important;\n}\n\n#toast-container>.toast-error {\n    background-image: none !important;\n}\n\n#toast-container>.toast-success {\n    background-image: none !important;\n}\n\n#toast-container>.toast-info {\n    background-image: none !important;\n}\n\n.bg-light, .btn-light {\n    background-color: transparent !important;\n}\n\n.select2-selection__choice {\n    background-color: var(--mdb-secondary) !important;\n    color: var(--mdb-button-color) !important;\n}\n\n.select2-container--default .select2-results__option--selected {\n    background-color: var(--mdb-secondary) !important;\n    color: var(--mdb-button-color) !important;\n}\n\n/* modal */\n.close {\n    color: var(--mdb-primary);  /* ensure close button is visible in both themes */\n}\n.close:hover {\n    color: var(--mdb-secondary);  /* ensure close button is visible in both themes */\n}\n\n.fs-1 {\n    font-size: 4rem !important;\n}\n\n/* plotly */\n.gtitle, .xtitle, .xtick text {\n    fill: var(--mdb-primary) !important;\n}\n\n/*override mdb <strong> font-weight */\nstrong {\n    font-weight: bolder;\n}\n\n.editable {\n    display: inline;\n}\n\n.bg-warning-dark {\n    background-color: #FF8800 !important;\n}\n\n.toast-top-right {\n    top: 4rem;\n}\n\n#toast-container>div {\n    color: var(--mdb-primary);\n}\n\n.quote {\n    border-left: 5px solid #1565C0;\n}\n\nhtml, body {\n    height: 100%;\n    min-height: 100%;\n}\n\n@media screen and (min-width: 768px) {  /*equivalent of bootstrap -md filter*/\n    .w-md-75 {\n        width: 75% !important;\n    }\n}\n\n@media screen and (min-width: 992px) {  /*equivalent of bootstrap -lg filter*/\n    .w-lg-50 {\n        width: 50% !important;\n    }\n}\n\n.login_box {\n    max-width: 30rem;\n}\n\n.brand-logo {\n    height: 100%;\n    width: 100%;\n    max-height: 2.5rem;\n    max-width: 2.5rem;\n}\n\n.navbar-logo {\n    height: 100%;\n    width: 100%;\n    max-height: 3rem;\n    max-width: 3rem;\n}\n\n.profile-overview-values {\n    font-weight: bold;\n}\n\n.profile-overview-explanation {\n    font-weight: inherit;\n}\n\n.profile-overview {\n    border: 2px solid black;\n}\n\n.vertically-aligned {\n    justify-content: space-between;\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n}\n\n.profile-overview:hover {\n    animation: all 0.2s ease-in forwards;\n    border-bottom: 2px solid white;\n    border-right: 2px solid white;\n}\n\n.profile-overview-selected {\n    border-bottom: 2px solid var(--mdb-secondary-500);\n    border-right: 2px solid var(--mdb-secondary-500);\n}\n\n.profile-overview-image {\n    max-height: 120px;\n    max-width: 120px;\n}\n\nfooter.page-footer {\n    color: var(--mdb-primary-text-emphasis);\n    background-color: var(--mdb-navbar-bg);\n    box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.05);\n}\n\n.navbar {\n    background-color: var(--mdb-navbar-bg);\n}\n\n.navbar .navbar-nav .nav-item:hover {\n    animation: all 0.2s ease-in forwards;\n    border-bottom: 2px solid var(--mdb-secondary);\n}\n\n.navbar .navbar-nav .nav-item.active {\n    border-bottom: 2px solid var(--mdb-secondary);\n}\n\n.dropdown-toggle::after {\n    margin-top: auto;\n    margin-bottom: auto;\n    transform: rotate(180deg);\n}\n\n.dropdown-toggle.collapsed::after {\n    margin-top: auto;\n    margin-bottom: auto;\n    transform: none;\n}\n\n.sidebar {\n    position: fixed;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 10; /* Behind the navbar */\n    padding: 48px 0 0; /* Height of navbar */\n    box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n    .sidebar-sticky {\n        position: -webkit-sticky;\n        position: sticky;\n    }\n}\n\n.sidebar-sticky {\n    position: relative;\n    top: 0;\n    height: calc(100vh - 48px);\n    padding-top: .5rem;\n    overflow-x: hidden;\n    overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */\n}\n\n.sidebar .nav-title {\n    margin-left: 2rem;\n}\n\n.separator {\n    border-bottom: 1px solid var(--mdb-secondary);\n}\n\n.btn.btn-sm.rounded-circle {\n    padding-left: 0.64rem;\n    padding-right: 0.64rem;\n    border-radius: 50%;\n}\n\n.font-size-90 {\n    font-size: 90% !important;\n}\n\n/* Cards */\n.card-deck {\n    display: flex;\n    justify-content: flex-start;\n    flex-flow: row wrap;\n    align-items: stretch;\n}\n\n.card {\n    display: block;\n    flex-basis: 33.3%;\n    . rounded-bottom !important;\n}\n\n.card.profile-card {\n    flex-basis: inherit !important;\n}\n\n.config-card {\n    border: var(--local-config-card--border-width) solid rgba(var(--mdb-primary-rgb), 0.3);\n}\n\n.community-bot-stats-label {\n  font-weight: bold;\n  font-size: x-large;\n  color: var(--mdb-primary);\n}\n\n.community-bot-stats {\n  font-size: large;\n  color: var(--mdb-button-color);\n  background-color: var(--mdb-secondary-500);\n}\n\n.package_row_image {\n    max-height: 4rem;\n}\n\n.table td.centered {\n    text-align: center;\n    vertical-align: middle;\n}\n\n.tentacle_package_action {\n    font-size: 1.5rem;\n}\n\n.blurred {\n    filter: blur(0.1rem);\n}\n\n.card,\n.block-card {\n    flex-basis: 100% !important;\n}\n@media screen and (min-width: 768px) {  /*equivalent of bootstrap -md filter*/\n    .card,\n    .block-card {\n        flex-basis: 50% !important;\n    }\n}\n\n.bg-darker {\n    background-color: #121212;\n}\n\n.text-danger-darker {\n    color: #C62828;\n}\n\n.disabled-text {\n    color: grey;\n}\n\n.medium-size {\n    max-width: 18rem;\n    min-width: 12rem;\n    justify-content: space-between;\n    display: flex;\n}\n\n.medium-size .card-body {\n    flex: 0 0 auto;\n}\n\n.small-size {\n    max-width: 18rem;\n    min-width: 12rem;\n    min-height: 14rem;\n}\n\n.very-small-size {\n    max-width: 3rem;\n    min-width: 2rem;\n    max-height: 3rem;\n    min-height: 2rem;\n}\n\n.cloud-logo {\n    max-width: 6rem;\n    min-width: 4rem;\n    max-height: 6rem;\n    min-height: 4rem;\n}\n\n.cloud-logo-2x {\n    max-height: 8rem;\n}\n\n.cloud-logo-4x {\n    max-height: 24rem;\n}\n\n.feature-margin {\n    margin-top: 10rem;\n}\n\n.img-feature {\n    max-height: 20rem;\n    margin: auto;\n    display: block;\n}\n\n/* Theme */\n/* Elegant */\n.card-body.candle-graph {\n    height: 30.625rem;\n    background-color: inherit;\n}\n\n.select2-results__group{\n    font-weight: bold;\n    text-align: center;\n    border-top: 1px solid #e9ecef;\n    font-size: .875rem;\n}\n\n.small-image {\n    max-height: 128px;\n    max-width: 128px;\n}\n\n.help-section {\n    padding-bottom: 0.4rem;\n}\n\n.fa-1_5x {\n    font-size: 1.5em;\n}\n\n.fa.fa-spinner {\n    font-size: 1.4em;\n}\n\n.fa.fa-spinner.fa-2xl {\n    font-size: 32px;\n}\n\nspan.large-editable div.editable-input {\n    width: 100%;\n}\n\na:hover.external-link {\n    font-weight: inherit;\n}\n\na:hover.hover_anim {\n    font-weight: inherit;\n    box-shadow: 0.1rem 0.1rem;\n}\n\na:hover.badge {\n    font-weight: 700;\n}\n\n.card a:link.button {\n    color: inherit;\n}\n\n.interface-screen {\n    filter: brightness(80%);\n}\n\n.editable-input,\n.editable-submit,\n.editable-cancel {\n    position: relative;\n    z-index: 2;\n}\n\n.bg-rating-1 {\n    background-color: rgba(1, 181, 116, 0.6);\n}\n\n.bg-rating-2 {\n    background-color: rgba(255, 181, 71, 0.6);\n}\n\n.bg-rating-3 {\n    background-color: rgba(238, 93, 80, 0.6);\n}\n\n.exchange-logo {\n    max-width: 85px;\n    height: auto;\n}\n\n.pointer-cursor {\n    cursor: pointer;\n}\n\n/* tables */\ntable.dataTable thead th,\ntable.dataTable tfoot th {\n    background: var(--mdb-table-headings-bg);\n    font-weight: normal;\n}\n\n/* cards */\n.card .deck-container-modified {\n    border: 2px solid var(--mdb-card-container-modified-border-color);\n}\n\n.card .card-modified,\n.card .card-deck .card-modified {\n    border: 2px solid var(--mdb-card-modified-border-color);\n}\n\n.card .card-deck .card-status-color {\n    -webkit-transition: all 0.5s ease;\n    -moz-transition: all 0.5s ease;\n    -o-transition: all 0.5s ease;\n    transition: all 0.5s ease;\n    border: 5px solid var(--mdb-card-status-color);\n}\n\n.card .card-deck .card-very-long {\n    border: 5px solid var(--mdb-card-very-long-border-color);\n}\n\n.card .card-deck .card-long {\n    border: 5px solid var(--mdb-card-long-border-color);\n}\n\n.card .card-deck .card-short {\n    border: 5px solid var(--mdb-card-short-border-color);\n}\n\n.card .card-deck .card-very-short {\n    border: 5px solid var(--mdb-card-very-short-border-color);\n}\n\n/* introjs */\n\n.introjs-tooltip {\n  background-color: rgba(000, 0, 0, 0.9);\n}\n\n.introjs-tooltip-title {\n  color: #fff;  /* avoid using h1 color */\n}\n\n/* markdown fixes */\npre code {\n    font-size: inherit;\n    color: var(--mdb-code-color);   /* 'inherit' overridden for themes compatibility */\n    word-break: normal;\n}"
  },
  {
    "path": "Services/Interfaces/web_interface/static/css/w2ui_template.css",
    "content": "\n/** todo clean up **/\n\n.w2ui-column-check {\n    background: #fff !important;\n    color: #131722 !important;\n}\n\n.w2ui-grid .w2ui-grid-body table .w2ui-col-number {\n    background-color: unset !important;\n}\n\n.w2ui-grid-data-spacer, .w2ui-grid-data  {\n    border-bottom: unset !important;\n}\n\n.w2ui-empty-record *, .w2ui-empty-record {\n    border: none !important;\n    box-shadow:none !important;\n}\n\n.w2ui-btn.close-btn {\n    padding: 24px 9px !important;\n    padding-top: 24px !important;\n}\n\n.w2ui-btn {\n    border-radius: 0 !important;\n    background-color: #131722 !important;\n    font-size: 1rem !important;\n    text-transform: unset !important;\n    padding: 8px 2rem !important;\n    border-color: #fb3 !important;\n    box-shadow: unset !important;\n    color: #fb3 ! important;\n}\n\n.w2ui-btn-blue {\n    color: #00c851 !important;\n    border-color: #00c851 !important;\n}\n\n.w2ui-btn {\n    margin: 0 !important;\n}\n\ninput.w2ui-input, .w2ui-overlay *, .w2ui-overlay select, .w2ui-btn {\n    background-color: #131722 !important;\n    color: #b2b5be !important;\n    border-color: #2a2e39 !important;\n    background-image: unset !important;\n}\n\n.w2ui-record, .w2ui-btn {\n    border: 1px solid var(--brdr) !important;\n}\n\n.w2ui-overlay tr:hover, .w2ui-grid-toolbar td:hover, .w2ui-grid-toolbar tr:hover, .w2ui-grid-toolbar table:hover, .w2ui-grid-toolbar tbody:hover, .w2ui-grid-toolbar table tbody tr:hover td, .w2ui-grid-toolbar table.dataTable tbody tr:hover td {\n    -moz-box-shadow: none !important;\n    -webkit-box-shadow: none !important;\n    box-shadow: none !important;\n}\n\n.w2ui-grid .w2ui-grid-body {\n    background-color: var(--bg);\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/distributions/market_making/js/configuration.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\n$(document).ready(function() {\n    const getAvailableCurrencies = () => {\n        const currencies = new Set()\n        exchangeSymbols.forEach(symbol => {\n            const baseAndQuote = symbol.split(\"/\");\n            currencies.add(baseAndQuote[0]);\n            currencies.add(baseAndQuote[1]);\n        })\n        return Array.from(currencies);\n    }\n\n    const fetchExchangeSymbols = async (exchange) => {\n        const url = $(\"#traded-symbol-selector\").data(\"update-url\")\n        const allSymbols = await async_send_and_interpret_bot_update(\n            null, `${url}/${exchange}`, null, \"GET\"\n        )\n        exchangeSymbols = allSymbols.filter(symbol => {\n            // ignore non spot symbols\n            return symbol.indexOf(\":\") === -1;\n        })\n    }\n\n    const saveConfig = async (saveUrl) => {\n        try {\n            validateConfig();\n            const updatedConfig = getConfigUpdate();\n            const resp = await async_send_and_interpret_bot_update(updatedConfig, saveUrl, null);\n            create_alert(\"success\", \"Configuration saved\", resp);\n            refreshExchangeSelector()\n            lastSavedConfig = updatedConfig\n            configEditor.validate()\n        } catch (error) {\n            create_alert(\"error\", \"Impossible to save config\", error)\n        }\n    }\n\n    const updateSymbols = async (exchange) => {\n        const previouslySelectedSymbol = getSelectedPair();\n        clearSymbolSelector();\n        await fetchExchangeSymbols(exchange);\n        const currencies = getAvailableCurrencies();\n        refreshSymbolSelector(previouslySelectedSymbol);\n        refreshPortfolioEditor(currencies);\n    }\n\n    const clearSymbolSelector = () => {\n        $(\"#traded-symbol-selector\").empty()\n    }\n\n    const refreshSymbolSelector = (previouslySelectedSymbol) => {\n        const symbolSelector = $(\"#traded-symbol-selector\");\n        let options = []\n        const profilePair = symbolSelector.data(\"selected-pair\");\n        const selectedValue = previouslySelectedSymbol === null ? profilePair: previouslySelectedSymbol;\n        options = options.concat(exchangeSymbols.sort().map((symbol) => {\n            return new Option(symbol, symbol, false, symbol===selectedValue);\n        }));\n        clearSymbolSelector()\n        symbolSelector.append(...options);\n    }\n\n    const refreshExchangeSelector = () => {\n        const exchanges = getSelectableExchange();\n        const exchangeSelector = $(\"#main-exchange-selector\");\n        const profileExchange = exchangeSelector.data(\"selected-exchange\");\n        const selectedValue = exchangeSelector.val() === null ? profileExchange: exchangeSelector.val();\n        const options = exchanges.map((exchange) => {\n            return new Option(exchange, exchange, false, exchange===selectedValue);\n        });\n        if(selectedValue !== null && exchanges.indexOf(selectedValue) === -1){\n            // previously selected value is not available anymore: select 1st value by default\n            options[0].selected = true;\n            onSelectedExchange(options[0].value);\n        }\n        exchangeSelector.empty()\n        exchangeSelector.append(...options);\n    }\n\n    const onSelectedExchange = async (exchange) => {\n        if(typeof exchange === \"string\") {\n            await updateSymbols(exchange);\n        }\n    }\n\n    const refreshPortfolioEditor = (currencies) => {\n        const editorDiv = $(\"#simulated-portfolio-editor\");\n        let value = editorDiv.data(\"config\");\n        if(typeof value === \"undefined\"){\n            return\n        }\n        const schema = editorDiv.data(\"schema\");\n        if(simulatedPortfolioEditor !== undefined) {\n            value = simulatedPortfolioEditor.getValue();\n            simulatedPortfolioEditor.destroy();\n        }\n        value.forEach((val) => {\n            if(currencies.indexOf(val.asset) === -1){\n                currencies.push(val.asset)\n            }\n        })\n        schema.items.properties.asset.enum = currencies.sort();\n        simulatedPortfolioEditor = new JSONEditor(editorDiv[0],{\n            schema: schema,\n            startval: value,\n            no_additional_properties: true,\n            prompt_before_delete: true,\n            disable_array_reorder: true,\n            disable_array_delete: false,\n            disable_array_delete_last_row: true,\n            disable_array_delete_all_rows: true,\n            disable_collapse: true,\n            disable_edit_json: true,\n            disable_properties: true,\n        })\n        simulatedPortfolioEditor.on('ready', () => {\n            readyEditors.portfolio = true\n            initLastSavedConfig();\n        })\n    }\n\n    const refreshTradingSimulatorEditor = () => {\n        const editorDiv = $(\"#trading-simulator-editor\");\n        let value = editorDiv.data(\"config\");\n        if(typeof value === \"undefined\"){\n            return\n        }\n        const schema = editorDiv.data(\"schema\");\n        if(tradingSimulatorEditor !== undefined) {\n            value = tradingSimulatorEditor.getValue();\n            tradingSimulatorEditor.destroy();\n        }\n        schema.options = {\n            titleHidden: true\n        }\n        tradingSimulatorEditor = new JSONEditor(editorDiv[0],{\n            schema: schema,\n            startval: value,\n            disable_collapse: true,\n            disable_edit_json: true,\n            disable_properties: true,\n        })\n        tradingSimulatorEditor.on('ready', () => {\n            readyEditors.simulator = true\n            initLastSavedConfig();\n        })\n    }\n\n    const refreshExchangesEditor = () => {\n        const editorDiv = $(\"#exchanges-editor\");\n        let value = editorDiv.data(\"config\");\n        if(typeof value === \"undefined\"){\n            return\n        }\n        const schema = editorDiv.data(\"schema\");\n        if(exchangesEditor !== undefined) {\n            exchangesEditor.destroy();\n        }\n        schema.options = {\n            titleHidden: true\n        }\n        const selectableExchanges = schema.items.properties.name.enum;\n        value.forEach((val) => {\n            if(selectableExchanges.indexOf(val.name) === -1){\n                selectableExchanges.push(val.name)\n            }\n        })\n        schema.id=\"exchangesConfig\"\n        exchangesEditor = new JSONEditor(editorDiv[0],{\n            schema: schema,\n            startval: value,\n            no_additional_properties: true,\n            prompt_before_delete: true,\n            disable_array_reorder: true,\n            disable_array_delete: false,\n            disable_array_delete_last_row: true,\n            disable_array_delete_all_rows: true,\n            disable_collapse: true,\n            disable_edit_json: true,\n            disable_properties: true,\n        })\n    }\n\n    const addCustomValidator = () => {\n        // Custom validators must return an array of errors or an empty array if valid\n        JSONEditor.defaults.custom_validators.push((schema, value, path) => {\n            const errors = [];\n            if (schema.id === \"exchangesConfig\" && path === \"root\") {\n                const newNames = value.map(value => value.name);\n                const duplicates = newNames.filter(\n                    (value, index) => newNames.indexOf(value) !== index && newNames.lastIndexOf(value) === index\n                );\n                if (duplicates.length) {\n                    // Errors must be an object with `path`, `property`, and `message`\n                    errors.push({\n                        path: path,\n                        property: '',\n                        message: `Each exchanges can only be listed once. Exchanges listed more than once: ${duplicates}.`\n                    });\n                }\n            }\n            if (schema.id === \"tentacleConfig\" && path === \"root\") {\n                const referenceExchange = value.reference_exchange;\n                if (referenceExchange !== undefined) {\n                    try {\n                        // in try catch in case getSelectableExchange is not yet available\n                        const listedExchanges = getSelectableExchange();\n                        if (listedExchanges.concat([\"local\"]).indexOf(referenceExchange) === -1){\n                            // Errors must be an object with `path`, `property`, and `message`\n                            errors.push({\n                                path: path,\n                                property: 'reference_exchange',\n                                message: `Reference exchange must be listed in exchange configurations or equal to \"local\". Listed exchanges are ${listedExchanges.join(', ')}.`\n                            });\n                        }\n                        if (referenceExchange === getSelectedExchange()){\n                            // \"local\" must be used to use the same exchange to trade and as reference price\n                            errors.push({\n                                path: path,\n                                property: 'reference_exchange',\n                                message: `Reference exchange must be set to \"local\" when equal to your selected exchange.`\n                            });\n                        }\n                    } catch (err) {\n                        console.error(err)\n                    }\n                }\n                const minSpread = value.min_spread;\n                const maxSpread = value.max_spread;\n                if(minSpread !== undefined && maxSpread !== undefined && minSpread >= maxSpread){\n                    errors.push({\n                        path: path,\n                        property: 'max_spread',\n                        message: `Max spread % must be larger than Min spread %.`\n                    });\n                }\n            }\n            return errors;\n        });\n    }\n\n    const initSelectedExchange = async () => {\n        refreshExchangeSelector();\n        await onSelectedExchange(getSelectedExchange())\n    }\n\n    const getSelectableExchange = () => {\n        if(exchangesEditor === undefined){\n            return []\n        }\n        return exchangesEditor.getValue().map(value => value.name)\n    }\n    const getSelectedExchange = () => {\n        return $(\"#main-exchange-selector\").val()\n    }\n\n    const getSelectedPair = () => {\n        return $(\"#traded-symbol-selector\").val()\n    }\n\n    const getTradingModeName = () => {\n        return $(\"#trading-mode-config-editor\").data(\"trading-mode-name\")\n    }\n\n    const registerEvents = () => {\n         $(\"#main-exchange-selector\").on(\n             \"change\", () => onSelectedExchange(getSelectedExchange())\n         )\n    }\n\n    const validateConfig = () => {\n        [configEditor, tradingSimulatorEditor, simulatedPortfolioEditor, exchangesEditor].forEach((editor) => {\n            if (editor === undefined) {\n                throw \"Editors are loading\"\n            }\n            const errors = editor.validate();\n            if (errors.length) {\n                throw JSON.stringify(errors.map(\n                    err => `${err.path.replace('root.', '')}: ${err.message}`\n                ).join(\", \"))\n            }\n        });\n        const exchange = getSelectedExchange()\n        if(exchange === undefined || exchange === null || !exchange.length){\n            throw \"No selected exchange\"\n        }\n        const pair = getSelectedPair()\n        if(pair === undefined || pair === null || !pair.length){\n            // can happen, don't prevent saving\n            create_alert(\"error\", \"Action required\", \"Please select a trading pair to start your strategy\");\n        }\n    }\n\n    const getConfigUpdate = () => {\n        return {\n            exchange: getSelectedExchange(),\n            tradingPair: getSelectedPair(),\n            tradingModeName: getTradingModeName(),\n            tradingModeConfig: configEditor.getValue(),\n            tradingSimulatorConfig: tradingSimulatorEditor.getValue(),\n            simulatedPortfolioConfig: simulatedPortfolioEditor.getValue(),\n            exchangesConfig: exchangesEditor.getValue(),\n        }\n    }\n\n    const initLastSavedConfig = () => {\n        if (\n            readyEditors.exchanges\n            && readyEditors.simulator\n            && readyEditors.portfolio\n            && lastSavedConfig === undefined\n        ) {\n            lastSavedConfig = getConfigUpdate()\n        }\n    }\n\n    const initUIWhenPossible = () => {\n        exchangesEditor.on('ready', () => {\n            initSelectedExchange();\n            registerEvents();\n            readyEditors.exchanges = true\n            initLastSavedConfig();\n        })\n        $(\"[data-role=save]\").on(\"click\", (event) => {\n            saveConfig($(event.currentTarget).data(\"update-url\"))\n        })\n    }\n\n    const hasPendingUpdates = () => {\n        if (tradingSimulatorEditor === undefined\n            || simulatedPortfolioEditor === undefined\n            || exchangesEditor === undefined\n            || lastSavedConfig === undefined\n        ) {\n            return false;\n        }\n        return getValueChangedFromRef(\n            getConfigUpdate(), lastSavedConfig, true\n        )\n    }\n\n    let exchangeSymbols = [];\n    let tradingSimulatorEditor = undefined;\n    let simulatedPortfolioEditor = undefined;\n    let exchangesEditor = undefined;\n    let lastSavedConfig = undefined\n    const readyEditors = {\n        exchanges: false,\n        simulator: false,\n        portfolio: false,\n    }\n\n\n    refreshExchangesEditor();\n    refreshTradingSimulatorEditor();\n    initUIWhenPossible();\n    addCustomValidator();\n    register_exit_confirm_function(hasPendingUpdates)\n    startTutorialIfNecessary(\"mm:configuration\");\n});"
  },
  {
    "path": "Services/Interfaces/web_interface/static/distributions/market_making/js/dashboard.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\n$(document).ready(function() {\n    startTutorialIfNecessary(\"mm:home\");\n});"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/backtesting_util.js",
    "content": "\n/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction start_backtesting(request, update_url, success_callback=null){\n    const success = success_callback === null ? start_success_callback : success_callback;\n    send_and_interpret_bot_update(request, update_url, null, success, start_error_callback);\n}\n\nfunction start_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    $(\"#progess_bar_anim\").css('width', 0+'%').attr(\"aria-valuenow\", 0);\n    create_alert(\"success\", msg, \"\");\n}\n\nfunction start_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n    create_alert(\"error\", result.responseText, \"\");\n    $(`#${backtestingMainProgressBar}`).hide();\n    lock_interface(false);\n}\n\nfunction lock_interface(lock=true){\n    let should_lock = lock;\n    if(!should_lock){\n        lock_interface_callbacks.forEach(function (value) {\n            if(value()){\n                should_lock = true;\n            }\n        });\n    }\n    $('#startBacktesting').prop('disabled', should_lock);\n}\n\nfunction load_report(report, should_alert=False) {\n    const reportDiv = $(\"#backtestingReport\");\n    if (reportDiv.length) {\n        const url = reportDiv.attr(update_url_attr);\n        $.get(url, (data) => {\n            if (\"report\" in data) {\n                let error_message = \"\";\n                const globalReport = data[\"report\"]\n                const botReport = globalReport[\"bot_report\"]\n                report.show();\n                const profitabilities = [];\n                const show_exchanges = Object.keys(botReport[\"profitability\"]).length > 1;\n                $.each(botReport[\"profitability\"], function (exchange, profitability) {\n                    const exch = show_exchanges ? `${exchange}: ` : \"\";\n                    profitabilities.push(`${exch}${profitability}`);\n                });\n                let profitability = profitabilities.join(\", \");\n                const errors_count = globalReport[\"errors_count\"];\n                if (\"error\" in globalReport || errors_count > 0) {\n                    error_message = \"Warning: error(s) during backtesting\";\n                    if (\"error\" in globalReport) {\n                        error_message += \" \" + globalReport[\"error\"];\n                    }\n                    if (errors_count > 0) {\n                        error_message += \" \" + errors_count + \" error(s)\";\n                    }\n                    error_message += \", more details in logs.\";\n                    if (should_alert) {\n                        create_alert(\"error\", error_message, \"\");\n                    }\n                    $(\"#backtestingErrorsAlert\").show();\n                } else {\n                    $(\"#backtestingErrorsAlert\").hide();\n                }\n\n                const symbol_reports = [];\n                $.each(globalReport[\"symbol_report\"], function (index, value) {\n                    $.each(value, function (symbol, profitability) {\n                        symbol_reports.push(`${symbol}: ${round_digits(profitability, 4)}%`);\n                    });\n                });\n                const all_profitability = symbol_reports.join(\", \");\n                $(\"#bProf\").html(`${round_digits(profitability, 4)}% ${error_message}`);\n                const avg_profitabilities = [];\n                $.each(botReport[\"market_average_profitability\"], function (exchange, market_average_profitability) {\n                    const exch = show_exchanges ? `${exchange}: ` : \"\";\n                    avg_profitabilities.push(`${exch}${round_digits(market_average_profitability, 4)}%`);\n                });\n                $(\"#maProf\").html(avg_profitabilities.join(\", \"));\n                $(\"#refM\").html(botReport[\"reference_market\"]);\n                $(\"#sProf\").html(all_profitability);\n                $(\"#reportTradingModeName\").html(botReport[\"trading_mode\"]);\n                $(\"#reportTradingModeNameLink\").attr(\"href\", $(\"#reportTradingModeNameLink\").attr(\"base_href\") + botReport[\"trading_mode\"]);\n                const end_portfolio_reports = [];\n                $.each(botReport[\"end_portfolio\"], function (exchange, portfolio) {\n                    let exchange_portfolio = show_exchanges ? `${exchange} ` : \"\";\n                    $.each(portfolio, function (symbol, holdings) {\n                        const digits = holdings[\"total\"] > 10 ? 2 : 10;\n                        exchange_portfolio = `${exchange_portfolio} ${symbol}: ${round_digits(holdings[\"total\"], digits)}`;\n                    });\n                    end_portfolio_reports.push(exchange_portfolio);\n                });\n                $(\"#ePort\").html(end_portfolio_reports.join(\", \"));\n                const starting_portfolio_reports = [];\n                $.each(botReport[\"starting_portfolio\"], function (exchange, portfolio) {\n                    let exchange_portfolio = show_exchanges ? `${exchange} ` : \"\";\n                    $.each(portfolio, function (symbol, holdings) {\n                        exchange_portfolio = `${exchange_portfolio} ${symbol}: ${holdings[\"total\"]}`;\n                    });\n                    starting_portfolio_reports.push(exchange_portfolio);\n                });\n                $(\"#sPort\").html(starting_portfolio_reports.join(\", \"));\n\n                last_chart_identifiers = globalReport[\"chart_identifiers\"]\n                fillTimeFrameSelector(last_chart_identifiers);\n                add_graphs(last_chart_identifiers);\n                add_tables(data[\"trades\"], botReport[\"reference_market\"]);\n\n            }\n        }).fail(function () {\n            report.hide();\n        }).always(function () {\n            report.attr(\"loading\", \"false\");\n        });\n    }\n}\n\n\nconst fillTimeFrameSelector = (chart_identifiers) => {\n    const selector = $(\"#timeFrameSelect\");\n    selector.empty();\n    if(!chart_identifiers.length){\n        return;\n    }\n    selector.append(...chart_identifiers[0][\"time_frames\"].map(\n        (tf, index) => new Option(tf, tf)\n    ));\n    selector.val(chart_identifiers[0][\"time_frames\"][0]);\n    selector.selectpicker('refresh');\n}\n\n\nconst registerTimeFrameSelector = () => {\n    $(\"#timeFrameSelect\").on(\"change\", () => {\n        add_graphs(last_chart_identifiers)\n    })\n}\n\n\nfunction add_graphs(chart_identifiers){\n    const result_graph_id = \"result-graph-\";\n    const graph_symbol_price_id = \"graph-symbol-price-\";\n    const result_graphs = $(\"#result-graphs\");\n    result_graphs.empty();\n    $.each(chart_identifiers, function (_, chart_identifier) {\n        const target_template = $(\"#\"+result_graph_id+config_default_value);\n        const symbol = chart_identifier[\"symbol\"];\n        const exchange_id = chart_identifier[\"exchange_id\"];\n        const exchange_name = chart_identifier[\"exchange_name\"];\n        const time_frame =  $(\"#timeFrameSelect\").val() ? $(\"#timeFrameSelect\").val() : chart_identifier[\"time_frames\"];\n        const graph_card = target_template.html().replace(new RegExp(config_default_value,\"g\"), exchange_id+symbol);\n        result_graphs.append(graph_card);\n        const formated_symbol = symbol.replace(new RegExp(\"/\",\"g\"), \"|\");\n        get_symbol_price_graph(`${graph_symbol_price_id}${exchange_id}${symbol}`, exchange_id, exchange_name, formated_symbol, time_frame, true, true);\n    })\n}\n\nconst add_tables = (trades, refMarket) => {\n    return displayTradesTable(\"result-trades\", trades, refMarket, true);\n}\n\nfunction updateBacktestingProgress(progress){\n    updateProgressBar(\"progess_bar_anim\", progress);\n}\n\nfunction refreshBacktestingStatus(){\n    backtestingSocket.emit('backtesting_status');\n}\n\nfunction init_backtesting_status_websocket(){\n    backtestingSocket = get_websocket(\"/backtesting\");\n    backtestingSocket.on('backtesting_status', function(backtesting_status_data) {\n        _handle_backtesting(backtesting_status_data);\n    });\n}\n\nfunction _handle_backtesting(backtesting_status_data){\n    const backtesting_status = backtesting_status_data[\"status\"];\n    const progress = backtesting_status_data[\"progress\"];\n    const errors = backtesting_status_data[\"errors\"];\n\n    const report = $(\"#backtestingReport\");\n    const progress_bar = $(`#${backtestingMainProgressBar}`);\n    const stopButton = $(\"#backtester-stop-button\");\n\n    if(backtesting_status === \"computing\" || backtesting_status === \"starting\"){\n        lock_interface(true);\n        progress_bar.show();\n        if(stopButton.length){\n            stopButton.removeClass(hidden_class);\n        }\n        updateBacktestingProgress(progress);\n        first_refresh_state = backtesting_status;\n        if(report.is(\":visible\")){\n            report.hide();\n        }\n        backtesting_computing_callbacks.forEach((callback) => callback());\n        // re-schedule progress refresh\n        setTimeout(function () {refreshBacktestingStatus()}, 50);\n    }\n    else{\n        lock_interface(false);\n        progress_bar.hide();\n        if(stopButton.length){\n            stopButton.addClass(hidden_class);\n        }\n        if(backtesting_status === \"finished\"){\n            const should_alert = first_refresh_state !== \"\" && first_refresh_state !== \"finished\";\n            if(should_alert){\n                create_alert(\"success\", \"Backtesting finished.\", \"\");\n                first_refresh_state=\"finished\";\n            }\n            if(!report.is(\":visible\") && report.attr(\"loading\") === \"false\"){\n                report.attr(\"loading\", \"true\");\n                load_report(report, should_alert);\n            }\n\n            if(previousBacktestingStatus === \"computing\" || previousBacktestingStatus === \"starting\") {\n                backtesting_done_callbacks.forEach((callback) => callback(errors));\n            }\n        }\n    }\n    if(first_refresh_state === \"\"){\n        first_refresh_state = backtesting_status;\n    }\n    previousBacktestingStatus = backtesting_status;\n}\n\nlet first_refresh_state = \"\";\n\nlet backtestingSocket = undefined;\nconst lock_interface_callbacks = [];\nconst backtesting_done_callbacks = [];\nconst backtesting_computing_callbacks = [];\nlet previousBacktestingStatus = undefined;\nlet backtestingMainProgressBar = \"backtesting_progress_bar\";\nlet last_chart_identifiers = [];\n\n$(document).ready(function() {\n   registerTimeFrameSelector();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/bot_connection.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction init_status_websocket(){\n    const onBotReconnected = () => _reconnectedCallbacks.forEach((callback) => callback());\n    const socket = get_websocket(\"/notifications\");\n    socket.on('update', (data) => {\n        if(_isBotDisconnected){\n            _isBotDisconnected = false;\n            onBotReconnected();\n        }\n        unlock_ui();\n        manage_alert(data);\n    });\n    socket.on('disconnect', async () => {\n        if(!_isBotDisconnected && await checkDisconnected()){\n            _isBotDisconnected = true;\n            lock_ui();\n        }\n    });\n}\n\nfunction manage_alert(data){\n    try{\n        const errors_count = data[\"errors_count\"];\n        const errorBadge = $(\"#errors-count-badge\");\n        if(errorBadge.length){\n            if(errors_count > 0){\n                errorBadge.text(errors_count);\n            }else{\n                errorBadge.text(\"\");\n            }\n        }\n        const notifications = data[\"notifications\"];\n        const maxDisplayedNotifications = 10;\n        // only display latest notifications when too many to display\n        const displayedNotifications = (\n            notifications.length > maxDisplayedNotifications ?\n            notifications.slice(notifications.length - maxDisplayedNotifications, notifications.length): notifications\n        );\n        $.each(displayedNotifications, (i, item) => {\n            const toastAlertLevel = item[\"Level\"] === \"critical\" ? \"error\": item[\"Level\"]\n            create_alert(toastAlertLevel, item[\"Title\"], item[\"Message\"], \"\", item[\"Sound\"]);\n            $.each(notificationCallbacks, (_, callback) => {\n               callback(item[\"Title\"], item);\n            });\n        })\n    }\n    catch(error) {\n        console.log(error);\n    }\n}\n\nfunction handle_route_button(){\n    $(\".btn\").each((_, jsButton) => {\n        if(!jsButton.hasAttribute('route')){\n            return;\n        }\n        $(jsButton).click((event) => {\n            const button = $(event.currentTarget);\n            if (button[0].hasAttribute('route')){\n                const command = button.attr('route');\n                const origin_val = button.text();\n                $.ajax({\n                    url: command,\n                    beforeSend: function() {\n                        button.html(\"<i class='fa fa-circle-notch fa-spin'></i>\");\n                    },\n                    success: function() {\n                        create_alert(\"info\", \"OctoBot is stopping\", \"\");\n                    },\n                    complete: function() {\n                       button.html(origin_val);\n                    }\n                });\n             }\n        });\n    });\n}\n\nfunction send_and_interpret_bot_update(updated_data, update_url, dom_root_element, success_callback, error_callback, method=\"POST\"){\n    $.ajax({\n        url: update_url,\n        type: method,\n        dataType: \"json\",\n        contentType: 'application/json',\n        data: JSON.stringify(updated_data),\n        success: function(msg, status){\n            if(typeof success_callback === \"undefined\") {\n                if(dom_root_element != null){\n                    update_dom(dom_root_element, msg);\n                }\n            }\n            else{\n                success_callback(updated_data, update_url, dom_root_element, msg, status)\n            }\n        },\n        error: function(result, status, error){\n            window.console&&console.error(result, status, error);\n            if(typeof error_callback === \"undefined\") {\n                let error_text = result.responseText.length > 1000 ? status : result.responseText;\n                create_alert(\"error\", \"Error when handling action: \"+error_text+\".\", \"\");\n            }\n            else{\n                error_callback(updated_data, update_url, dom_root_element, result, status, error);\n            }\n        }\n    })\n}\n\nconst async_send_and_interpret_bot_update = async (updated_data, update_url, dom_root_element, method=\"POST\", alertOnFailure=true) => {\n    return new Promise((resolve, reject) => {\n        const success = (updated_data, update_url, dom_root_element, msg, status) => {\n            resolve(msg);\n        }\n        const failure = (updated_data, update_url, dom_root_element, msg, status) => {\n            if(alertOnFailure){\n                generic_request_failure_callback(updated_data, update_url, dom_root_element, msg, status)\n            }\n            reject(msg);\n        }\n        send_and_interpret_bot_update(updated_data, update_url, dom_root_element, success, failure, method);\n    });\n}\n\nconst notificationCallbacks = [];\nconst _reconnectedCallbacks = [];\nlet _isBotDisconnected = false;\n\nfunction isBotDisconnected(){\n    return _isBotDisconnected;\n}\n\nasync function checkDisconnected(){\n    try {\n        await async_send_and_interpret_bot_update(null, $(\"#resources-urls\").data(\"ping-url\"), null, \"GET\", false);\n        return false;\n    } catch (err) {\n        return true;\n    }\n}\n\nfunction register_notification_callback(callback){\n    notificationCallbacks.push(callback);\n}\n\nfunction registerReconnectedCallback(callback){\n    _reconnectedCallbacks.push(callback);\n}\n\n$(document).ready(function () {\n    handle_route_button();\n\n    init_status_websocket();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/candlesticks.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction get_symbol_price_graph(element_id, exchange_id, exchange_name, symbol, time_frame, display_orders, backtesting=false,\n                                replace=false, should_retry=false, attempts=0,\n                                data=undefined, success_callback=undefined, no_data_callback=undefined){\n    if(isDefined(data)){\n        create_or_update_candlestick_graph(element_id, data, symbol, exchange_name, time_frame, replace);\n    }else{\n        const backtesting_enabled = backtesting ? \"backtesting\" : \"live\";\n        const ajax_url = \"/dashboard/currency_price_graph_update/\"+ exchange_id +\"/\" + symbol + \"/\"\n            + time_frame + \"/\" + backtesting_enabled + \"?display_orders=\" + display_orders;\n        $.ajax({\n            url: ajax_url,\n            type: \"GET\",\n            dataType: \"json\",\n            contentType: 'application/json',\n            success: function(data, status){\n                if(data !== null && \"error\" in data && data[\"error\"].includes(\"no data for\")){\n                    if(isDefined(no_data_callback)) {\n                        no_data_callback(element_id);\n                    }\n                }else if (!create_or_update_candlestick_graph(element_id, data, symbol, exchange_name, time_frame, replace)){\n                    if (should_retry && attempts < max_attempts){\n                        const marketsElement = $(\"#loadingMarketsDiv\");\n                        marketsElement.removeClass(disabled_item_class);\n                        setTimeout(function(){\n                            marketsElement.addClass(disabled_item_class);\n                            get_symbol_price_graph(element_id, exchange_id, exchange_name, symbol, time_frame, display_orders, backtesting, replace, should_retry,attempts+1, data, success_callback);\n                        }, 3000);\n                    }\n                }else{\n                    const loadingSelector = $(\"div[name='loadingSpinner']\");\n                    if (loadingSelector.length) {\n                        $.each(loadingSelector, function () {\n                            $(this).addClass(disabled_item_class);\n                        });\n                    }\n                    if(isDefined(success_callback)){\n                        success_callback();\n                    }\n                }\n            },\n            error: function(result, status, error){\n                window.console&&console.error(error, result, status);\n                const loadingSelector = $(\"div[name='loadingSpinner']\");\n                if (loadingSelector.length) {\n                    loadingSelector.addClass(hidden_class);\n                }\n                $(document.getElementById(element_id)).html(`<h7>Error when loading graph: ${error} [${result.responseText}]. More details in logs.</h7>`)\n            }\n        });\n    }\n}\n\nfunction get_first_symbol_price_graph(element_id, in_backtesting_mode=false, callback=undefined, time_frame=undefined, display_orders=true) {\n    const url = $(\"#first_symbol_graph\").attr(update_url_attr);\n    $.get(url,function(data) {\n        if($.isEmptyObject(data)){\n            // no exchange data available yet, retry soon, bot must be starting\n            setTimeout(function(){\n                get_first_symbol_price_graph(element_id, in_backtesting_mode, callback, time_frame, display_orders);\n            }, 300);\n        }else{\n            if(\"time_frame\" in data){\n                let formatted_symbol = data[\"symbol\"].replace(new RegExp(\"/\",\"g\"), \"|\");\n                const fetched_time_frame = time_frame ? time_frame : data[\"time_frame\"];\n                get_symbol_price_graph(element_id, data[\"exchange_id\"], data[\"exchange_name\"], formatted_symbol,\n                    fetched_time_frame, display_orders, in_backtesting_mode, false, true,\n                    0, undefined, function () {\n                        if(isDefined(callback)){\n                            callback(data[\"exchange_id\"], data[\"symbol\"], data[\"time_frame\"], element_id);\n                        }\n                    });\n            }\n        }\n    });\n}\n\nfunction get_watched_symbol_price_graph(element, callback=undefined, no_data_callback=undefined, time_frame=undefined, display_orders=true) {\n    const symbol = element.attr(\"symbol\");\n    let formatted_symbol = symbol.replace(new RegExp(\"/\",\"g\"), \"|\");\n    const ajax_url = \"/dashboard/watched_symbol/\"+ formatted_symbol;\n    $.get(ajax_url,function(data) {\n        if(\"time_frame\" in data){\n            const fetched_time_frame = time_frame ? time_frame : data[\"time_frame\"];\n            let formatted_symbol = data[\"symbol\"].replace(new RegExp(\"/\",\"g\"), \"|\");\n            get_symbol_price_graph(element.attr(\"id\"), data[\"exchange_id\"], data[\"exchange_name\"], formatted_symbol,\n                fetched_time_frame, display_orders, false, false, true,\n                0, undefined, function () {\n                    if(isDefined(callback)){\n                        callback(data[\"exchange_id\"], data[\"symbol\"], data[\"time_frame\"], element.attr(\"id\"));\n                    }\n                }, no_data_callback);\n        }else if($.isEmptyObject(data)){\n            // OctoBot is starting, try again\n            const marketsElement = $(\"#loadingMarketsDiv\");\n            marketsElement.removeClass(disabled_item_class);\n            setTimeout(function(){\n                get_watched_symbol_price_graph(element, callback, no_data_callback, time_frame, display_orders);\n            }, 1000);\n        }\n    });\n}\n\nconst stop_color = getComputedStyle(document.body).getPropertyValue('--local-price-chart-stop-color');\nconst sell_color = getComputedStyle(document.body).getPropertyValue('--local-price-chart-sell-color');\nconst buy_color = getComputedStyle(document.body).getPropertyValue('--local-price-chart-buy-color');\nconst candle_sell_color = getComputedStyle(document.body).getPropertyValue('----local-price-chart-candle-sell-color');\nconst candle_buy_color = getComputedStyle(document.body).getPropertyValue('--local-price-chart-candle-buy-color');\n\nfunction create_candlesticks(candles){\n    const data_time = candles[\"time\"];\n    const data_close = candles[\"close\"];\n    const data_high = candles[\"high\"];\n    const data_low = candles[\"low\"];\n    const data_open = candles[\"open\"];\n\n    return {\n      x: data_time,\n      close: data_close,\n      decreasing: {line: {color: candle_sell_color}},\n      high: data_high,\n      increasing: {line: {color: candle_buy_color}},\n      line: {color: 'rgba(31,119,180,1)'},\n      low: data_low,\n      open: data_open,\n      type: 'candlestick',\n      name: 'Prices',\n      xaxis: 'x',\n      yaxis: 'y2'\n    };\n}\n\nfunction create_volume(candles){\n\n    const data_time = candles[\"time\"];\n    const data_close = candles[\"close\"];\n    const data_volume = candles[\"vol\"];\n    \n    const colors = [];\n    $.each(data_close, function (i, value) {\n        if(i !== 0) {\n            if (value > data_close[i - 1]) {\n                colors.push(buy_color);\n            }else{\n                colors.push(sell_color);\n            }\n        }\n        else{\n            colors.push(sell_color);\n        }\n\n    });\n\n    return {\n          x: data_time,\n          y: data_volume,\n          marker: {\n              color: colors\n          },\n          type: 'bar',\n          name: 'Volume',\n          xaxis: 'x',\n          yaxis: 'y1'\n    };\n}\n\nfunction create_trades(trades, trader){\n\n    if (isDefined(trades) && isDefined(trades[\"time\"]) && trades[\"time\"].length > 0) {\n        const data_time = trades[\"time\"];\n        const data_price = trades[\"price\"];\n        const data_trade_description = trades[\"trade_description\"];\n        const data_order_side = trades[\"order_side\"];\n\n        const marker_size = 16;\n        const marker_opacity =  0.9;\n        const border_line_color = getTextColor();\n        const colors = [];\n        $.each(data_order_side, function (index, value) {\n            colors.push(_getOrderColor(trades[\"trade_description\"][index], value));\n        });\n\n        const line_with = isDarkTheme() ? 1 : 0.2;\n\n        return {\n            x: data_time,\n            y: data_price,\n            mode: 'markers',\n            name: \"\",\n            text: data_trade_description,\n            hovertemplate: `%{text}<br>%{x}`,\n            marker: {\n                color: colors,\n                size: marker_size,\n                opacity: marker_opacity,\n                line: {\n                    width: line_with,\n\t\t\t\t\tcolor: border_line_color\n                }\n            },\n            xaxis: 'x',\n            yaxis: 'y2'\n        }\n    }else{\n        return {}\n    }\n}\n\nconst _getOrderColor = (orderDesc, side) => {\n    if(orderDesc.includes(\"STOP\")){\n        return stop_color;\n    }\n    return side === \"sell\" ? sell_color : buy_color\n}\n\nfunction create_orders(orders, trader, firstTime, lastTime){\n    const firstDate = new Date(`20${firstTime}`)\n    if (isDefined(orders) && isDefined(orders.time) && orders.time.length > 0) {\n        return orders.time.map((x, index) => {\n            return {\n              x: [new Date(`20${x}`) >= firstDate ? x : firstTime, lastTime],\n              y: [orders.price[index], orders.price[index]],\n              mode: 'lines+markers',\n              text: orders.description[index],\n              hoverinfo: \"text\",\n              line: {\n                dash: 'dashdot',\n                width: 2,\n                color: _getOrderColor(orders.description[index], orders.order_side[index]),\n              },\n              marker: {\n                  symbol: \"star-diamond\",\n              },\n              xaxis: 'x',\n              yaxis: 'y2'\n            }\n        });\n    }else{\n        return []\n    }\n}\n\nfunction update_trades(trades, trader_name, reference_trades){\n    if(isDefined(reference_trades) && isDefined(reference_trades.y)){\n        if(isDefined(trades.time) && trades.time.length){\n            const new_trades = create_trades(trades, trader_name);\n            if(new_trades.mode){\n                for(let i=0; i<new_trades.x.length; i++){\n                    reference_trades.x.push(new_trades.x[i]);\n                    reference_trades.y.push(new_trades.y[i]);\n                    reference_trades.text.push(new_trades.text[i]);\n                    reference_trades.marker.color.push(new_trades.marker.color[i]);\n                }\n            }\n        }\n    }else{\n        reference_trades = create_trades(trades, trader_name)\n    }\n    return reference_trades;\n}\n\nfunction update_last_candle(to_update_candles, to_update_vols, new_candles, last_price_trace_index, last_candle_index){\n    to_update_candles.open[last_price_trace_index] = new_candles[\"open\"][last_candle_index];\n    to_update_candles.high[last_price_trace_index] = new_candles[\"high\"][last_candle_index];\n    to_update_candles.low[last_price_trace_index] = new_candles[\"low\"][last_candle_index];\n    to_update_candles.close[last_price_trace_index] = new_candles[\"close\"][last_candle_index];\n    to_update_vols.y[last_price_trace_index] = new_candles[\"vol\"][last_candle_index];\n    const prev_vol_color = new_candles[\"close\"][last_candle_index] >= new_candles[\"open\"][last_candle_index] ?\n        buy_color : sell_color;\n    to_update_vols.marker.color[last_price_trace_index] = prev_vol_color;\n}\n\nfunction create_layout(graph_title){\n    return {\n        title: graph_title,\n        dragmode: isMobileDisplay() ? false : 'zoom',\n        margin: {\n            r: 10,\n            t: 25,\n            b: 40,\n            l: 60\n        },\n        showlegend: false,\n        xaxis: {\n            autorange: true,\n            domain: [0, 1],\n            title: 'Date',\n            type: 'date',\n            rangeslider: {\n                visible: false,\n            }\n        },\n        yaxis1: {\n            domain: [0, 0.2],\n            title: 'Volume',\n            autorange: true,\n            showgrid: false,\n            showticklabels: false\n        },\n        yaxis2: {\n            domain: [0.2, 1],\n            autorange: true,\n            title: 'Price',\n            gridcolor: `rgba(${getTextColorRGB()}, 0.2)`,\n        },\n        paper_bgcolor: 'rgba(0,0,0,0)',\n        plot_bgcolor: 'rgba(0,0,0,0)',\n        font: {\n            color: getTextColor(),\n        }\n    };\n}\n\nfunction push_new_candle(price_trace, volume_trace, candles, candle_index, last_candle_time){\n    price_trace.x.push(last_candle_time);\n    price_trace.open.push(candles[\"open\"][candle_index]);\n    price_trace.high.push(candles[\"high\"][candle_index]);\n    price_trace.low.push(candles[\"low\"][candle_index]);\n    price_trace.close.push(candles[\"close\"][candle_index]);\n    volume_trace.y.push(candles[\"vol\"][candle_index]);\n    const vol_color = candles[\"close\"][candle_index] >= candles[\"open\"][candle_index] ?\n        buy_color : sell_color;\n    volume_trace.marker.color.push(vol_color);\n}\n\nfunction create_or_update_candlestick_graph(element_id, symbol_price_data, symbol, exchange_name, time_frame, replace=false){\n    if (symbol_price_data) {\n        const candles = symbol_price_data[\"candles\"];\n        const trades = symbol_price_data[\"trades\"];\n        const orders = symbol_price_data[\"orders\"];\n        const isSimulated = symbol_price_data[\"simulated\"]\n\n        let layout = undefined;\n\n        let price_trace = undefined;\n        let volume_trace = undefined;\n\n        let real_trader_trades = undefined;\n        let simulator_trades = undefined;\n\n        let plotted_orders = undefined;\n\n        const prev_data = document.getElementById(element_id);\n        const prev_layout = prev_data.layout;\n\n        if (prev_layout && !replace) {\n            volume_trace = prev_data.data[0];\n            price_trace = prev_data.data[1];\n            real_trader_trades = prev_data.data[2];\n            simulator_trades = prev_data.data[3];\n\n            // keep layout\n            layout = prev_layout;\n            // update data revision to force graph update\n            layout.datarevision = layout.datarevision + 1;\n\n            // trades\n            real_trader_trades = isSimulated ? real_trader_trades : update_trades(trades, \"Real trader\", real_trader_trades);\n            simulator_trades = isSimulated ? update_trades(trades, \"Simulator\", simulator_trades) : simulator_trades;\n\n            // candles\n            if(isDefined(candles) && isDefined(candles.time) && candles.time.length){\n                const last_price_trace_index = price_trace.close.length - 1;\n                const last_candle_index = candles[\"close\"].length - 1;\n                const last_candle_time = candles[\"time\"][last_candle_index];\n\n                if (last_candle_index > 0){\n                    // Candle update with last candle being and in-construction candle\n                    if (price_trace.x[last_price_trace_index] !== last_candle_time) {\n                        update_last_candle(price_trace, volume_trace, candles, last_price_trace_index, last_candle_index - 1);\n                        push_new_candle(price_trace, volume_trace, candles, last_candle_index, last_candle_time);\n                    } else {\n                        update_last_candle(price_trace, volume_trace, candles, last_price_trace_index, last_candle_index);\n                    }\n                } else if(price_trace.x[last_price_trace_index].indexOf(last_candle_time) === -1) {\n                    // Candle update with only one candle but this candle is not displayed (no in-construction candle)\n                    push_new_candle(price_trace, volume_trace, candles, last_candle_index, last_candle_time);\n                }\n            }\n        }\n        if(!isDefined(layout)){\n            let graph_title = symbol;\n            if (exchange_name !== \"ExchangeSimulator\") {\n                graph_title = graph_title + \" (\" + exchange_name + \", time frame: \" + time_frame + \")\";\n            }\n            layout = create_layout(graph_title);\n        }\n        if(!isDefined(price_trace)){\n            price_trace = create_candlesticks(candles);\n        }\n        if(!isDefined(volume_trace)){\n            volume_trace = create_volume(candles);\n        }\n        if(!isDefined(real_trader_trades)){\n            real_trader_trades = isSimulated ? [] : create_trades(trades, \"Real trader\");\n        }\n        if(!isDefined(simulator_trades)){\n            simulator_trades = isSimulated ? create_trades(trades, \"Simulator\") : [];\n        }\n        const lastTime = price_trace.x[price_trace.x.length - 1];\n        const firstTime = price_trace.x[0];\n        plotted_orders = create_orders(orders, isSimulated ? \"Simulator\": \"Real trader\", firstTime, lastTime);\n\n        const data = [volume_trace, price_trace, real_trader_trades, simulator_trades, ...plotted_orders];\n        const plotlyConfig = {\n            staticPlot: isMobileDisplay(),\n            scrollZoom: false,\n            modeBarButtonsToRemove: [\"select2d\", \"lasso2d\", \"toggleSpikelines\"],\n            responsive: true,\n            showEditInChartStudio: true,\n            displaylogo: false // no logo to avoid 'rel=\"noopener noreferrer\"' security issue (see https://webhint.io/docs/user-guide/hints/hint-disown-opener/)\n        };\n        if(replace){\n            Plotly.newPlot(element_id, data, layout, plotlyConfig);\n        }else{\n            Plotly.react(element_id, data, layout, plotlyConfig);\n        }\n        return true;\n    }else{\n        return false\n    }\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/common_handlers.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    const backButton = $(\"#back-button\");\n    if(backButton){\n        backButton.click(function (){\n            historyGoBack();\n        });\n    }\n    const reloadButton = $(\"#reload-button\");\n    if(reloadButton){\n        reloadButton.click(function (){\n            location.reload();\n        });\n    }\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/cst.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n// dom data attributes and classes\nconst config_key_attr = \"config-key\";\nconst config_value_attr = \"config-value\";\nconst current_value_attr = \"current-value\";\nconst startup_value_attr = \"startup-config-value\";\nconst update_url_attr = \"update-url\";\nconst config_type_attr = \"config-type\";\nconst config_data_type_attr = \"data-type\";\nconst config_root_class = \"config-root\";\nconst config_container_class = \"config-container\";\nconst config_element_class = \"config-element\";\nconst no_activation_click_attr = \"no-activation-click\";\n\n// dom display classes\nconst success_badge = \"badge-success\";\nconst warning_badge = \"badge-warning\";\nconst secondary_badge = \"badge-secondary\";\nconst primary_badge = \"badge-primary\";\nconst modified_badge = \"badge-modified\";\nconst modified_class = \"warning-color\";\nconst selected_item_class = \"selected-item\";\nconst disabled_item_class = \"d-none\";\nconst hidden_class = \"d-none\";\nconst disabled_class = \"disabled-item\";\n\nconst card_class_modified = \"card-modified\";\nconst deck_container_modified_class = \"deck-container-modified\";\nconst deck_container_class = \"deck-container\";\nconst config_card_class = \"config-card\";\nconst added_class = \"new_element\";\n\nconst light_list_item = \"list-group-item-light\";\nconst success_list_item = \"list-group-item-success\";\n\nconst activation_pending = \"Activation pending restart\";\nconst deactivation_pending = \"Deactivation pending restart\";\nconst unsaved_setting = \"Unsaved setting\";\nconst activated = \"Activated\";\nconst deactivated = \"Deactivated\";\n\nconst config_default_value = \"Bitcoin\";\nconst config_default_symbol = \"btc\";\n\nconst evaluator_config_type = \"evaluator_config\";\nconst evaluator_list_config_type = \"evaluator_list_config\";\nconst trading_config_type = \"trading_config\";\nconst tentacles_config_type = \"tentacle_config\";\n\nconst max_attempts = 20;\n\nconst price_graph_update_interval = 3000;\nconst profitability_update_interval = 5000;\nconst portfolio_update_interval = 60000;\n\nconst mobile_width_breakpoint = 1024;\nconst medium_width_breakpoint = 1400;\n\nconst material_colors = [\"#2962ff\", \"#00b8d4\", \"#00c853\", \"#aeea00\", \"#ffab00\",\n    \"#dd2c00\", \"#d50000\", \"#6200ea\"];\n\nconst material_dark_colors = [\"#0039cb\", \"#0088a3\", \"#009624\", \"#79b700\", \"#c67c00\",\n    \"#a30000\", \"#9b0000\", \"#0a00b6\"];\n\nconst TimeFramesMinutes = {\n    \"1m\": 1,\n    \"3m\": 3,\n    \"5m\": 5,\n    \"15m\": 15,\n    \"30m\": 30,\n    \"1h\": 60,\n    \"2h\": 120,\n    \"3h\": 180,\n    \"4h\": 240,\n    \"6h\": 360,\n    \"8h\": 480,\n    \"12h\": 720,\n    \"1d\": 1440,\n    \"3d\": 4320,\n    \"1w\": 10080,\n    \"1M\": 43200,\n    \"1y\": 524160,\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/custom_elements.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction create_circular_progress_doughnut(element, label1=\"% Done\", label2=\"% Remaining\"){\n    return new Chart(element.getContext('2d'), {\n        type: 'doughnut',\n        data: {\n            labels: [label1, label2],\n            datasets: [\n                {\n                    data: [0, 100],\n                    backgroundColor: [\"#F7464A\",\"#949FB1\"],\n                    hoverBackgroundColor: [\"#FF5A5E\", \"#A8B3C5\"]\n                }\n            ]\n        },\n        options: {\n            responsive: true,\n            cutoutPercentage: 80\n        }\n    });\n}\n\nfunction create_doughnut_chart(element, data, title, displayLegend=true, graphHeight=400, update){\n    const labels = [];\n    const values = [];\n    const backgroundColors = [];\n    let index = 0;\n    let totalValue = 0;\n    $.each(data, function (_, value) {\n        totalValue += value;\n    });\n    $.each(data, function (key, value) {\n        if(value > 0){\n            values.push(value);\n            labels.push(`${key} ${(value/totalValue*100).toFixed(2)}%`);\n            const color = get_color(index);\n            backgroundColors.push(color);\n            index += 1;\n        }\n    });\n    const plottedData = [{\n        values: values,\n        labels: labels,\n        marker: {\n            colors: backgroundColors\n        },\n        textinfo: 'none',\n        type: \"pie\"\n    }]\n    const layout = {\n        height: graphHeight,\n        legend: {\n            orientation: isMobileDisplay() ? \"h\": \"v\",\n            font: {\n                color: getTextColor()\n            }\n        },\n        showlegend: displayLegend,\n        margin: {\n            t: 0,\n            b: 0,\n        },\n        paper_bgcolor: 'rgba(0,0,0,0)',\n    }\n    const plotlyConfig = {\n        scrollZoom: false,\n        responsive: true,\n        displayModeBar: false\n    };\n    if (update){\n        // Plotly.restyle(element, plottedData); // todo use restyle for better perf\n        Plotly.newPlot(element, plottedData, layout, plotlyConfig);\n    } else {\n        Plotly.newPlot(element, plottedData, layout, plotlyConfig);\n    }\n}\n\nfunction create_line_chart(element, data, title, fontColor='white', update=true, height=undefined){\n    const trace = {\n      x: data.map((e) => new Date(e.time*1000)),\n      y: data.map((e) => e.value),\n      fill: \"tonexty\",\n      type: 'scatter',\n      line: {shape: 'spline'},\n    };\n    const minY = Math.min.apply(null, trace.y);\n    const maxDisplayY = Math.max.apply(null, trace.y);\n    const minDisplayY = Math.max(0, minY - ((maxDisplayY - minY) / 2));\n    const titleSpecs = {\n        text: title,\n        font: {\n            size: 24\n        },\n    };\n    const layout = {\n        title: titleSpecs,\n        height: height,\n        dragmode: isMobileDisplay() ? false : 'zoom',\n        margin: {\n            l: 30,\n            r: 0,\n            t: 40,\n            b: 40,\n        },\n        xaxis: {\n            autorange: true,\n            showgrid: false,\n            domain: [0, 1],\n            type: 'date',\n            rangeslider: {\n                visible: false,\n            },\n            automargin: true,\n        },\n        yaxis1: {\n            showgrid: false,\n            range: [minDisplayY, maxDisplayY],\n            automargin: true,\n        },\n        paper_bgcolor: 'rgba(0,0,0,0)',\n        plot_bgcolor: 'rgba(0,0,0,0)',\n        font: {\n            color: fontColor\n        },\n    };\n    const plotlyConfig = {\n        staticPlot: isMobileDisplay(),\n        scrollZoom: false,\n        modeBarButtonsToRemove: [\"select2d\", \"lasso2d\", \"toggleSpikelines\"],\n        responsive: true,\n        showEditInChartStudio: false,\n        displaylogo: false // no logo to avoid 'rel=\"noopener noreferrer\"' security issue (see https://webhint.io/docs/user-guide/hints/hint-disown-opener/)\n    };\n    if(update){\n        const layoutUpdate = {\n            title: titleSpecs\n        }\n        Plotly.update(element, {x: [trace.x], y: [trace.y]}, layoutUpdate, 0);\n    } else {\n        Plotly.newPlot(element, [trace], layout, plotlyConfig);\n    }\n}\n\nfunction create_histogram_chart(element, data, titleY1, titleY2, nameYAxis, fontColor='gray', update=true){\n    const trace1 = {\n      x: data.map((e) => new Date(e.time*1000)),\n      y: data.map((e) => e.y1),\n      marker: {\n         color: getTextColor(),\n      },\n      opacity: 0.9,\n      line: {\n        width: 4,\n      },\n      type: 'scatter',\n      name: titleY1,\n    };\n    // rgb(198,40,40) octobot red\n    // rgb(0,142,0) green\n    const trace2 = {\n      x: data.map((e) => new Date(e.time*1000)),\n      y: data.map((e) => e.y2),\n      marker: {\n         color: data.map((e) => e.y2 > 0 ? 'rgba(0,142,0,.8)': 'rgba(198,40,40,.5)'),\n          line: {\n            color: data.map((e) => e.y2 > 0 ? 'rgb(0,142,0)': 'rgb(198,40,40)'),\n            width: 1.5,\n          }\n      },\n      yaxis: 'y2',\n      type: 'bar',\n      name: titleY2,\n    };\n    const maxDisplayY = Math.max(0, Math.max.apply(null, trace2.y) * 1.5);\n    const minDisplayY = Math.min(0, Math.min.apply(null, trace2.y) * 1.5);\n    const layout = {\n        dragmode: isMobileDisplay() ? false : 'zoom',\n        xaxis: {\n            autorange: true,\n            showgrid: false,\n            domain: [0, 1],\n            type: 'date',\n            rangeslider: {\n                visible: false,\n            }\n        },\n        yaxis1: {\n            showgrid: true,\n            overlaying: 'y2',\n            title: nameYAxis,\n            rangemode: \"tozero\",\n            gridcolor: \"grey\"\n        },\n        yaxis2: {\n            rangemode: \"tozero\",\n            showgrid: false,\n            showticklabels: false,\n            side: 'right',\n            range: [minDisplayY, maxDisplayY],\n        },\n        paper_bgcolor: 'rgba(0,0,0,0)',\n        plot_bgcolor: 'rgba(0,0,0,0)',\n        font: {\n            color: fontColor\n        },\n        margin: {\n            l: 30,\n            r: 30,\n            t: 40,\n        },\n        showlegend: false,\n    };\n    const plotlyConfig = {\n        staticPlot: isMobileDisplay(),\n        scrollZoom: false,\n        modeBarButtonsToRemove: [\"select2d\", \"lasso2d\", \"toggleSpikelines\"],\n        responsive: true,\n        showEditInChartStudio: false,\n        displaylogo: false // no logo to avoid 'rel=\"noopener noreferrer\"' security issue (see https://webhint.io/docs/user-guide/hints/hint-disown-opener/)\n    };\n    if(update){\n        Plotly.restyle(element, {x: [trace1.x], y: [trace1.y], y2: [trace2.y]}, 0);\n    } else {\n        Plotly.newPlot(element, [trace1, trace2], layout, plotlyConfig);\n    }\n}\n\nfunction update_circular_progress_doughnut(chart, done, remaining){\n    chart.data.datasets[0].data[0] = done;\n    chart.data.datasets[0].data[1] = remaining;\n    chart.update();\n}\n\nfunction create_bars_chart(element, labels, datasets, min_y=0, displayLegend=true, fontColor='white', zeroLineColor='black'){\n    return new Chart(element.getContext('2d'), {\n        type: 'bar',\n        data: {\n            labels: labels,\n            datasets: datasets\n        },\n        options: {\n            responsive: true,\n            legend: {\n                display: displayLegend,\n                labels: {\n                    fontColor: fontColor,\n                    fontSize: 15\n                }\n            },\n            scales:{\n                xAxes:[{\n                    ticks:{\n                          fontColor: fontColor,\n                          fontSize: 14\n                    }\n                }],\n                yAxes:[{\n                    ticks:{\n                        fontColor: fontColor,\n                        fontSize: 14,\n                        suggestedMin: min_y\n                    },\n                    gridLines:{\n                        zeroLineColor: zeroLineColor\n                    }\n                }]\n            }\n        }\n    });\n}\n\nfunction update_bars_chart(chart, datasets){\n    chart.data.datasets[0].data = datasets[0].data;\n    chart.data.datasets[0].backgroundColor = datasets[0].backgroundColor;\n    chart.data.datasets[0].borderColor = datasets[0].borderColor;\n    chart.update();\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/data_collector_util.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction lock_collector_ui(lock=true){\n    if(lock){\n        $(`#${collectorMainProgressBar}`).show();\n        // reset progress bar\n        $(\"#total_progess_bar_anim\").css('width', 0+'%').attr(\"aria-valuenow\", 0);\n    }else if(collectorHideProgressBarWhenFinished){\n        $(`#${collectorMainProgressBar}`).hide();\n    }\n    $('#collect_data').prop('disabled', lock);\n    $('#stop_collect_data').prop('disabled', !lock);\n}\n\nfunction _refreshDataCollectorStatus(socket){\n    socket.emit('data_collector_status');\n}\n\nfunction updateDataCollectorProgress(current_progress, total_progress){\n    if(current_progress === 0){\n        $(\"#progress_bar_anim-container\").hide();\n    }else{\n        $(\"#progress_bar_anim-container\").show();\n    }\n    $(\"#current_progess_bar_anim\").css('width', (current_progress === 0 ? 100 : current_progress)+'%').attr(\"aria-valuenow\", current_progress);\n    $(\"#total_progess_bar_anim\").css('width', total_progress+'%').attr(\"aria-valuenow\", total_progress);\n    $(\"#progess_bar_anim\").css('width', current_progress+'%').attr(\"aria-valuenow\", current_progress);\n}\n\nfunction init_data_collector_status_websocket(){\n    const socket = get_websocket(\"/data_collector\");\n    socket.on('data_collector_status', function(data_collector_status_data) {\n        _handle_data_collector_status(data_collector_status_data, socket);\n    });\n}\n\nfunction _handle_data_collector_status(data_collector_status_data, socket){\n    const data_collector_status = data_collector_status_data[\"status\"];\n    const current_progress = data_collector_status_data[\"progress\"][\"current_step_percent\"];\n    const total_progress = Math.round((data_collector_status_data[\"progress\"][\"current_step\"] \n                                    / data_collector_status_data[\"progress\"][\"total_steps\"]) * 100);\n    const stopButton = $(\"#collector-stop-button\");\n\n    if(data_collector_status === \"collecting\" || data_collector_status === \"starting\"){\n        if(stopButton.length){\n            stopButton.removeClass(hidden_class);\n        }\n        lock_collector_ui(true);\n        updateDataCollectorProgress(current_progress, total_progress);\n        DataCollectorCollectingCallbacks.forEach((callback) => callback());\n        // re-schedule progress refresh\n        setTimeout(function () {_refreshDataCollectorStatus(socket);}, 100);\n    }\n    else{\n        if(stopButton.length){\n            stopButton.addClass(hidden_class);\n        }\n        lock_collector_ui(false);\n        DataCollectorDoneCallbacks.forEach((callback) => callback());\n    }\n    collectorBacktestingStatus = data_collector_status;\n}\n\nconst DataCollectorDoneCallbacks = [];\nconst DataCollectorCollectingCallbacks = [];\nlet collectorBacktestingStatus = undefined;\nlet collectorHideProgressBarWhenFinished = true;\nlet collectorMainProgressBar = \"collector_operation\";\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/dom_updater.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction update_badge(badge, new_text, new_class){\n    badge.removeClass(secondary_badge);\n    badge.removeClass(warning_badge);\n    badge.removeClass(success_badge);\n    badge.removeClass(primary_badge);\n    badge.removeClass(modified_badge);\n    badge.addClass(new_class);\n    if (new_class === primary_badge){\n        badge.addClass(modified_badge);\n    }\n    badge.html(new_text);\n}\n\nfunction update_list_item(list_item, new_class){\n    list_item.removeClass(light_list_item);\n    list_item.removeClass(success_list_item);\n    list_item.addClass(new_class);\n}\n\nfunction update_element_required_marker_and_usability(element, display_marker) {\n    const marker = element.children(\"[role='required-flag']\");\n    if(display_marker){\n        marker.removeClass(hidden_class);\n        element.removeClass(disabled_class);\n        element.removeClass(disabled_item_class);\n    }else{\n        marker.addClass(hidden_class);\n        element.addClass(disabled_class);\n        element.addClass(disabled_item_class);\n    }\n}\n\nfunction update_element_temporary_look(element){\n    const set_to_activated = element.attr(current_value_attr).toLowerCase() === \"true\";\n    const set_to_temporary = element.attr(current_value_attr).toLowerCase() !== element.attr(config_value_attr).toLowerCase();\n    const is_back_to_startup_value = element.attr(startup_value_attr).toLowerCase() === element.attr(config_value_attr).toLowerCase();\n    if(element.hasClass(\"list-group-item\")){\n        // list item\n        const list_class = (set_to_activated ? success_list_item : light_list_item);\n        update_list_item(element, list_class);\n    }\n    const badge = element.find(\".badge\");\n    if(typeof badge !== \"undefined\") {\n        if(set_to_temporary){\n            update_badge(badge, unsaved_setting, primary_badge);\n        }else{\n            if(set_to_activated){\n                if (!is_back_to_startup_value){\n                    update_badge(badge, activation_pending, warning_badge);\n                }else{\n                    update_badge(badge, activated, success_badge);\n                }\n            }else{\n                if (!is_back_to_startup_value){\n                    update_badge(badge, deactivation_pending, warning_badge);\n                }else{\n                    update_badge(badge, deactivated, secondary_badge);\n                }\n            }\n        }\n    }\n}\n\nfunction change_boolean(to_update_element, new_value, new_value_string){\n    const badge = to_update_element.find(\".badge\");\n    const startup_value = to_update_element.attr(startup_value_attr).toLowerCase();\n    const is_back_to_startup_value = startup_value === new_value_string;\n    if(new_value){\n        update_list_item(to_update_element, success_list_item);\n        if (!is_back_to_startup_value){\n            update_badge(badge, activation_pending, warning_badge);\n        }else{\n            update_badge(badge, activated, success_badge);\n        }\n    }else{\n        update_list_item(to_update_element, light_list_item);\n        if (!is_back_to_startup_value){\n            update_badge(badge, deactivation_pending, warning_badge);\n        }else{\n            update_badge(badge, deactivated, secondary_badge);\n        }\n    }\n}\n\nfunction update_activated_deactivated_tentacles(root_element, message, element_type){\n    const config_value_attr = \"config-value\";\n\n    for (const conf_key in message[element_type]) {\n        const new_value = message[element_type][conf_key];\n        const new_value_type = \"boolean\";\n        const new_value_string = new_value.toString();\n        const to_update_element = root_element.find(\"#\"+conf_key);\n\n        const attr = to_update_element.attr(config_value_attr);\n\n        if (isDefined(attr)) {\n            if (attr.toLowerCase() !== new_value_string){\n                to_update_element.attr(config_value_attr, new_value_string);\n                if(new_value_type === \"boolean\"){\n                    const bool_val = new_value.toLowerCase() === \"true\";\n                    change_boolean(to_update_element, bool_val, new_value_string);\n                }\n\n            }\n        }else{\n            // todo find cards to update using returned data\n            to_update_element.removeClass(modified_class);\n        }\n\n    }\n}\n\nfunction update_dom(root_element, message){\n    // update global configuration\n    const super_container = $(\"#super-container\");\n    confirm_all_modified_classes(super_container);\n\n    // update evaluators config\n    update_activated_deactivated_tentacles(root_element, message, \"evaluator_updated_config\");\n\n    // update trading config\n    update_activated_deactivated_tentacles(root_element, message, \"trading_updated_config\");\n\n    // update tentacles config\n    update_activated_deactivated_tentacles(root_element, message, \"tentacle_updated_config\");\n}\n\nfunction create_alert(a_level, a_title, a_msg, url=\"_blank\", sound=null){\n    toastr[a_level](a_msg, a_title);\n    if(sound !== null){\n        new Audio(getAudioMediaUrl(sound)).play();\n    }\n\n    toastr.options = {\n      \"closeButton\": false,\n      \"debug\": false,\n      \"newestOnTop\": false,\n      \"progressBar\": false,\n      \"positionClass\": \"toast-top-right\",\n      \"preventDuplicates\": false,\n      \"onclick\": null,\n      \"showDuration\": 300,\n      \"hideDuration\": 1000,\n      \"timeOut\": 5000,\n      \"extendedTimeOut\": 1000,\n      \"showEasing\": \"swing\",\n      \"hideEasing\": \"linear\",\n      \"showMethod\": \"fadeIn\",\n      \"hideMethod\": \"fadeOut\"\n    }\n}\n\nfunction lock_ui(){\n    $(\"#main-nav-bar\").find($(\".nav-link\")).addClass(\"disabled\");\n    update_status(false);\n}\n\nfunction unlock_ui(){\n    $(\".nav-link\").removeClass(\"disabled\");\n    update_status(true);\n}\n\nfunction update_status(status){\n    const icon_status = $(\"#navbar-bot-status\");\n\n    // create alert if required\n    if (status && icon_status.hasClass(\"fa-times-circle\")){\n        create_alert(\"success\", \"Reconnected to Octobot\", \"\");\n    }else if(!status && icon_status.hasClass(\"fa-check\")){\n        create_alert(\"error\", \"Connection lost with Octobot\", \"Reconnecting...\");\n    }\n\n    // update central status\n    if (status){\n        icon_status.removeClass(\"fa-times-circle icon-black\");\n        icon_status.addClass(\"fa-check\");\n        icon_status.attr(\"title\",\"OctoBot operational\");\n    }else{\n        icon_status.removeClass(\"fa-check\");\n        icon_status.addClass(\"fa-times-circle icon-black\");\n        icon_status.attr(\"title\",\"OctoBot offline\");\n    }\n}\n\nfunction register_exit_confirm_function(check_function) {\n    const exit_event = 'beforeunload';\n    $(window).bind(exit_event, function(){\n      if(check_function()){\n          return \"Exit without saving ?\";\n      }\n    });\n}\n\nfunction remove_exit_confirm_function(){\n    const exit_event = 'beforeunload';\n    $(window).off(exit_event);\n}\n\n\nfunction confirm_all_modified_classes(container){\n    container.find(\".\"+deck_container_modified_class).each(function () {\n        toggle_class($(this), deck_container_modified_class, false);\n    });\n    container.find(\".\"+card_class_modified).each(function () {\n        toggle_class($(this), card_class_modified, false);\n    });\n    container.find(\".\"+added_class).each(function () {\n        toggle_class($(this), added_class, false);\n    });\n}\n\nfunction toggle_class(elem, class_type, toogle=true){\n    if(toogle && !elem.hasClass(class_type)){\n        elem.addClass(class_type, 500);\n    }else if(!toogle && elem.hasClass(class_type)){\n        elem.removeClass(class_type);\n    }\n}\n\nfunction toogle_deck_container_modified(container, modified=true) {\n    toggle_class(container, deck_container_modified_class, modified);\n}\n\nfunction toogle_card_modified(card, modified=true) {\n    toggle_class(card, card_class_modified, modified);\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/exchange_accounts.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\nfunction register_exchanges_checks(check_existing_accounts){\n    const update_exchanges_details = (exchangeCard, exchangeData) => {\n        const unloggedSupportingIcon = $(exchangeCard.find(\"[data-role=supporting-exchange]\"));\n        const supportingIcon = $(exchangeCard.find(\"[data-role=supporting-account]\"));\n        const validIcon = $(exchangeCard.find(\"[data-role=valid-account]\"));\n        const warnDetailsWrapper = $(exchangeCard.find(\"[data-role=account-warning-details-wrapper]\"));\n        const warnDetails = $(exchangeCard.find(\"[data-role=account-warning-details]\"));\n\n        warnDetailsWrapper.addClass(hidden_class);\n        const exchangeType = exchangeData[\"exchange_type\"]\n        const newToolTip = `Login successful using ${exchangeType} account`\n        // both have to be changed\n        validIcon.attr(\"title\", newToolTip)\n        validIcon.attr(\"data-original-title\", newToolTip)\n\n\n        if(exchangeData[\"supporting_exchange\"]){\n            if(exchangeData[\"auth_success\"]){\n                supportingIcon.removeClass(hidden_class);\n                unloggedSupportingIcon.addClass(hidden_class);\n            }else{\n                supportingIcon.addClass(hidden_class);\n                unloggedSupportingIcon.removeClass(hidden_class);\n            }\n        }\n        if(exchangeData[\"auth_success\"]){\n            validIcon.removeClass(hidden_class);\n        }else{\n            validIcon.addClass(hidden_class);\n            if(exchangeData[\"configured_account\"]) {\n                warnDetailsWrapper.removeClass(hidden_class);\n                warnDetails.text(exchangeData[\"error_message\"]);\n            }\n        }\n    }\n\n    const check_accounts = (exchangeCards) => {\n        const exchangesReq = {};\n        const apiKey = \"Empty\";\n        const apiSecret = apiKey;\n        const apiPassword = apiKey;\n        exchangeCards.forEach((exchangeCard) => {\n            const exchange = exchangeCard.find(\".card-body\").attr(\"name\");\n            if(exchange !== config_default_value && typeof exchange !== \"undefined\") {\n                exchangesReq[exchange] = {\n                    exchange: exchange,\n                    apiKey: apiKey,\n                    apiSecret: apiSecret,\n                    apiPassword: apiPassword,\n                    sandboxed: exchangeCard.find(`#exchange_${exchange}_sandboxed`).is(':checked')\n                };\n            }\n        })\n        if(!Object.keys(exchangesReq).length){\n            return;\n        }\n        $.post({\n            url: $(\"#exchange-container\").attr(update_url_attr),\n            data: JSON.stringify(exchangesReq),\n            contentType: 'application/json',\n            dataType: \"json\",\n            success: function(data, status){\n                exchangeCards.forEach((exchangeCard) => {\n                    const exchange = exchangeCard.find(\".card-body\").attr(\"name\");\n                    if(typeof data[exchange] !== \"undefined\"){\n                        update_exchanges_details(exchangeCard, data[exchange]);\n                    }\n                });\n            },\n            error: function(result, status, error){\n                window.console&&console.error(`Impossible to check the exchange accounts compatibility: ${result.responseText}. More details in logs.`);\n            }\n        })\n    }\n\n\n    const check_account = (exchangeCard, source, newValue) => {\n        const exchange = exchangeCard.find(\".card-body\").attr(\"name\");\n        if(exchange !== config_default_value && exchangeCard.find(\"#exchange_api-key\").length > 0){\n            const apiKey = source.attr(\"id\") === \"exchange_api-key\" ? newValue : exchangeCard.find(\"#exchange_api-key\").editable('getValue', true).trim();\n            const apiSecret = source.attr(\"id\") === \"exchange_api-secret\" ? newValue : exchangeCard.find(\"#exchange_api-secret\").editable('getValue', true).trim();\n            const apiPassword = source.attr(\"id\") === \"exchange_api-password\" ? newValue : exchangeCard.find(\"#exchange_api-password\").editable('getValue', true).trim();\n            const sandboxed = exchangeCard.find(`#exchange_${exchange}_sandboxed`).is(':checked');\n            $.post({\n                url: $(\"#exchange-container\").attr(update_url_attr),\n                data: JSON.stringify({\n                    exchange: {\n                        \"exchange\": exchange,\n                        \"apiKey\": apiKey,\n                        \"apiSecret\": apiSecret,\n                        \"apiPassword\": apiPassword,\n                        \"sandboxed\": sandboxed,\n                    }\n                }),\n                contentType: 'application/json',\n                dataType: \"json\",\n                success: function(data, status){\n                    update_exchanges_details(exchangeCard, data[exchange]);\n                },\n                error: function(result, status, error){\n                    window.console&&console.error(`Impossible to check the exchange account compatibility: ${result.responseText}. More details in logs.`);\n                }\n            })\n        }\n    }\n\n    const exchange_account_check = (e, params) => {\n        const element = $(e.target);\n        element.data(\"changed\", true);\n        check_account(element.parents(\"div[data-role=exchange]\"), element,\n            typeof params === \"undefined\" ? null : params.newValue);\n    }\n\n    const register_edit_events = () => {\n        const cards = [];\n        $(\"div[data-role=exchange]\").each(function (){\n            const card = $(this);\n            const inputs = card.find(\"a[data-type=text]\");\n            if(inputs.length){\n                add_event_if_not_already_added(inputs, 'save', exchange_account_check);\n            }\n            const bools = card.find(\"input[data-type=bool]\");\n            if(bools.length){\n                add_event_if_not_already_added(bools, 'change', exchange_account_check);\n            }\n            cards.push(card);\n        });\n        if(check_existing_accounts){\n            check_accounts(cards);\n        }\n    }\n\n    register_edit_events(check_existing_accounts);\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/feedback.js",
    "content": "function displayFeedbackForm(formId, userId, updateUrl) {\n    Tally.openPopup(\n        formId,\n        {\n            hiddenFields: {\n                userId: userId,\n            },\n            layout: \"modal\",\n            hideTitle: true,\n            width: 375,\n            emoji: {\n              text: \"👋\",\n              animation: \"wave\"\n            },\n            autoClose: 0,\n            onSubmit: (payload) => {\n                const filedFormDetails = {\n                    form_id: formId,\n                    user_id: userId,\n                }\n                send_and_interpret_bot_update(filedFormDetails, updateUrl, null, generic_request_success_callback)\n            },\n        }\n    );\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/json_editor_settings.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n// set bootstrap 4 theme for JSONEditor (https://github.com/json-editor/json-editor#css-integration)\nJSONEditor.defaults.options.iconlib = 'fontawesome5';\n// custom octobot theme\nclass OctoBotTheme extends JSONEditor.defaults.themes.bootstrap4 {\n  getButton(text, icon, title) {\n    const el = super.getButton(text, icon, title);\n    el.classList.remove(\"btn-secondary\");\n    el.classList.add(\"btn-sm\", \"btn-primary\", \"waves-effect\", \"px-2\", \"px-md-4\");\n    return el;\n  }\n  getCheckbox() {\n    const el = this.getFormInputField('checkbox');\n    el.classList.add(\"custom-control-input\");\n    return el;\n  }\n  getCheckboxLabel(text) {\n    const el = this.getFormInputLabel(text);\n    el.classList.add(\"custom-control-label\");\n    return el;\n  }\n  getFormControl(label, input, description) {\n    const group = document.createElement(\"div\");\n\n    if (label && input.type === \"checkbox\") {\n      group.classList.add(\"checkbox\", \"custom-control\", \"custom-switch\");\n      group.appendChild(input);\n      group.appendChild(label);\n    } else {\n      group.classList.add(\"form-group\");\n      if (label) {\n        label.classList.add(\"form-control-label\");\n        group.appendChild(label);\n      }\n      group.appendChild(input);\n    }\n\n    if (description) group.appendChild(description);\n\n    return group;\n  }\n  getIndentedPanel () {\n    const el = document.createElement('div')\n    el.classList.add('card', 'card-body', 'mb-3', \"px-1\", \"px-md-3\")\n\n    if (this.options.object_background) {\n      el.classList.add(this.options.object_background)\n    }\n\n    if (this.options.object_text) {\n      el.classList.add(this.options.object_text)\n    }\n\n    /* for better twbs card styling we should be able to return a nested div */\n\n    return el\n  }\n}\n\n\n// custom delete confirm prompt\nclass ConfirmArray extends JSONEditor.defaults.editors.array {\n  askConfirmation() {\n    if (this.jsoneditor.options.prompt_before_delete === true) {\n      if (confirm(\"Remove this element ?\") === false) {\n        return false;\n      }\n    }\n    return true;\n  }\n}\n\nJSONEditor.defaults.themes.octobot = OctoBotTheme;\nJSONEditor.defaults.editors.array = ConfirmArray;\nJSONEditor.defaults.options.theme = 'octobot';\nJSONEditor.defaults.options.required_by_default = true;\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/on_load.js",
    "content": "// Functions required in each page\n\nconst onPageLoad = () => {\n    // should be run before document is ready\n    const updateStartupMessages = () => {\n        if(!isMobileDisplay()){\n            $(\"#startup-messages-collapse-control\").attr(\"aria-expanded\", \"true\");\n            $(\"#startup-messages-collapse\").addClass(\"show\");\n        }\n    }\n    updateStartupMessages();\n}\nonPageLoad();\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/pnl_history.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nconst loadPnlFullChartHistory = (data, update) => {\n    const unit = $(\"#pnl_historyChart\").data(\"unit\");\n    const parentDiv = $(`#pnl_historyChart`);\n    if(data.length > 1){\n        parentDiv.removeClass(hidden_class);\n        let total_pnl = 0;\n        const chartedData = data.map((element) => {\n            total_pnl += element.pnl;\n            return {\n                time: element.ex_t,\n                y1: total_pnl,\n                y2: element.pnl,\n            }\n        })\n        create_histogram_chart(\n            document.getElementById(\"pnl_historyChart\"), chartedData, `cumulated profit/loss`, \"profit/loss\", unit, getTextColor(), false\n        );\n    }else{\n        parentDiv.addClass(hidden_class);\n    }\n}\n\nconst loadPnlTableHistory = (data, update) => {\n    let total_pnl = 0;\n    const hasDetails = data.length && data[0].d !== null;\n    const rows = data.map((element) => {\n        total_pnl += element.pnl;\n        if(hasDetails){\n            return [\n                {\n                    timestamp: element.d.en_t,\n                    date: element.d.en_d,\n                    side: element.d.en_s,\n                    base: element.d.b,\n                    quote: element.q,\n                    symbol: element.d.s,\n                    exchange: element.d.ex,\n                    trades: element.tc,\n                    amount: round_digits(element.d.en_a, 8),\n                    price: round_digits(element.d.en_p, 8),\n                    total : round_digits(element.d.en_a * element.d.en_p, 8),\n                },\n                {\n                    timestamp: element.ex_t,\n                    date: element.ex_d,\n                    side: element.d.ex_s,\n                    base: element.d.b,\n                    quote: element.q,\n                    symbol: element.d.s,\n                    exchange: element.d.ex,\n                    trades: element.tc,\n                    amount: round_digits(element.d.ex_a, 8),\n                    price: round_digits(element.d.ex_p, 8),\n                    total : round_digits(element.d.ex_a * element.d.ex_p, 8)\n                },\n                round_digits(element.pnl, 8),\n                {\n                    special: element.d.s_f.map(e => {return {f: round_digits(e.f, 8), c: e.c}}),\n                    amount: round_digits(element.d.f, 8),\n                    quote: element.q,\n                },\n            ]\n        }else{\n            return [\n                {timestamp: element.ex_t, date: element.ex_d, quote: element.q},\n                round_digits(element.pnl, 8),\n                round_digits(total_pnl, 8),\n                round_digits(element.pnl_a, 8),\n            ]\n        }\n    });\n    const pnlTable = $(\"#pnl_historyTable\");\n    const unit = rows.length ? rows[0][0].quote : pnlTable.data(\"unit\");\n    let previousOrder = [[0, \"desc\"]];\n    if(update){\n        const previousDataTable = pnlTable.DataTable();\n        previousOrder = previousDataTable.order();\n        previousDataTable.destroy();\n    }\n\n    const getSideBadge = (side) => {\n        return `<span class=\"badge font-size-90 badge-${side === 'sell' ? 'danger': 'success'}\">${side}</span>`\n    }\n\n    const getBoldRender = (amount) => {\n        return `<span class=\"font-weight-bold\">${amount}</span>`\n    }\n\n    const getPnlOrdersDetails = (data) => {\n        return `<span data-toggle=\"tooltip\" title=\"symbol: ${data.symbol} exchange: ${data.exchange} trades: ${data.trades}\">${getSideBadge(data.side)} ${data.date}: ${getBoldRender(data.amount)} ${data.base} at ${getBoldRender(data.price)}, total ${getBoldRender(data.total)}</span>`;\n    }\n\n    const columns = (\n        hasDetails ? [\n            {\n                title: `Entry (${unit})`,\n                render: (data, type) => {\n                    if (type === 'display' || type === 'filter') {\n                        return getPnlOrdersDetails(data);\n                    }\n                    return data.timestamp;\n                },\n                width: \"39%\",\n            },\n            {\n                title: `Close (${unit})`,\n                render: (data, type) => {\n                    if (type === 'display' || type === 'filter') {\n                        return getPnlOrdersDetails(data);\n                    }\n                    return data.timestamp;\n                },\n                width: \"39%\",\n            },\n            {\n                title: `${unit} PNL`,\n                width: \"11%\",\n            },\n            {\n                title: 'Total fees',\n                render: (data, type) => {\n                    if (type === 'display' || type === 'filter') {\n                        const base = data.amount ? `${data.amount} ${data.quote}${data.special.length ?' + ' : ''}` : \"\";\n                        const special = data.special.length ? data.special.map(e => `${e.f} ${e.c}`).join(\", \") : \"\";\n                        return `${base}${special}`\n                    }\n                    return data.amount;\n                },\n                width: \"11%\",\n            },\n        ] : [\n            {\n                title: 'Closing time',\n                render: (data, type) => {\n                    if (type === 'display' || type === 'filter') {\n                        return data.date\n                    }\n                    return data.timestamp;\n                },\n                width: \"25%\",\n            },\n            {\n                title: `${unit} Profit and Loss`,\n                width: \"25%\",\n            },\n            {\n                title: `Cumulated ${unit} Profit and Loss`,\n                width: \"25%\",\n            },\n            {\n                title: `${unit} traded volume`,\n                width: \"25%\",\n            },\n        ]\n    );\n    pnlTable.DataTable({\n        data: rows.reverse(),\n        columns: columns,\n        order: previousOrder,\n    });\n}\n\nconst fetchPnlHistory = async (scale, pair) => {\n    const url = $(\"#pnl_historyChart\").data(\"url\");\n    if(typeof url === \"undefined\"){\n        return [];\n    }\n    return await async_send_and_interpret_bot_update(null, `${url}${scale}&symbol=${pair}`, null, \"GET\", true)\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/portfolio_history.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    const createHistoricalPortfolioChart = (element_id, reference_market, update) => {\n        const element = $(`#${element_id}`);\n        const selectedTimeFrame = \"1d\"; // todo add timeframe selector\n        const url = `${element.data(\"url\")}${selectedTimeFrame}`;\n        const success = (updated_data, update_url, dom_root_element, msg, status) => {\n            const graphDiv = $(`#profitability_graph`);\n            const defaultDiv = $(`#no_profitability_graph`);\n            const height = isMobileDisplay()? 250 : isMediumDisplay() ? 450 : undefined;\n            if(msg.length > 1){\n                graphDiv.removeClass(hidden_class);\n                defaultDiv.addClass(hidden_class);\n                const current_value = msg[msg.length - 1].value;\n                const title = `${current_value > 0 ? current_value : '-'} ${reference_market}`\n                create_line_chart(document.getElementById(element_id), msg, title, 'white', update, height);\n            }else{\n                graphDiv.addClass(hidden_class);\n                defaultDiv.removeClass(hidden_class);\n            }\n        }\n        send_and_interpret_bot_update(null, url, null, success, generic_request_failure_callback, \"GET\");\n    }\n\n    const displayPortfolioHistory = (elementId, referenceMarket, update) => {\n        createHistoricalPortfolioChart(elementId, referenceMarket, update);\n    }\n\n    const update_display = (update) => {\n        const elementId = \"portfolio_historyChart\";\n        const referenceMarket = $(`#${elementId}`).data(\"reference-market\");\n        displayPortfolioHistory(elementId, referenceMarket, update);\n    }\n\n    const start_periodic_refresh = () => {\n        setInterval(function() {\n            update_display(true, true);\n        }, profitability_update_interval);\n    }\n\n    let firstLoad = true;\n    update_display(false);\n    if(firstLoad){\n        start_periodic_refresh();\n    }\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/required.js",
    "content": "// Functions required in each page\n\n$(document).ready(function() {\n    const initTooltips = () => {\n        $('[data-toggle=\"tooltip\"]').tooltip();\n    }\n\n    const registerThemeSwitch = async () => {\n        $(\"#theme-switch\").click(async () => {\n            const url = $(\"#theme-switch\").data(\"update-url\")\n            const otherColorMode = $(\"html\").data(\"mdb-theme\") === \"light\" ? \"dark\" : \"light\"\n            const data = {\n                \"color_mode\": otherColorMode\n            }\n            await async_send_and_interpret_bot_update(data, url)\n            location.reload();\n        })\n    }\n\n    initTooltips();\n    registerThemeSwitch();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/resources_rendering.js",
    "content": "/*\n * Drakkar-Software OctoBot-Tentacles\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nconst mardownConverter = new showdown.Converter();\nconst currentURL = `${window.location.protocol}//${window.location.host}`;\n\nfunction markdown_to_html(text) {\n    return mardownConverter.makeHtml(\n        text?.trim().replaceAll(\"<br><br>\", \"\\n\\n\")\n    )\n}\n\nfunction fetch_images() {\n    $(\".product-logo\").each(function () {\n        const element = $(this);\n        if(element.attr(\"src\") === \"\"){\n            $.get(`${currentURL}/${element.attr(\"url\")}`, function(data) {\n                element.attr(\"src\", data[\"image\"]);\n                element.removeClass(hidden_class)\n                const parentLink = element.parent(\"a\");\n                if (parentLink.attr(\"href\") === \"\"){\n                    parentLink.attr(\"href\", data[\"url\"]);\n                }\n            });\n        }\n    });\n}\n\nfunction handleDefaultImage(element, url){\n    const imgSrc = element.attr(\"src\");\n    element.on(\"error\",function () {\n        if (imgSrc !== url){\n            element.attr(\"src\", url);\n        }\n    });\n    if (((element[0].complete && element[0].naturalHeight === 0) && imgSrc !== url) || imgSrc.endsWith(currencyLoadingImageName)){\n        element.attr(\"src\", url);\n    }\n}\n\nlet currencyIdByName = undefined;\nlet currencyIdBySymbol = undefined;\nlet currencyDetails = []\nlet currencyLogoById = {};\nlet fetchedCurrencyIds = false;\n\nconst currencyLoadingImageName = \"loading_currency.svg\";\nconst currencyDefaultImage = `${currentURL}/static/img/svg/default_currency.svg`;\nconst currencyListURL = `${currentURL}/api/currency_list`;\nconst currencyLogoURL = `${currentURL}/currency_logos`;\n\n\nfunction fetchCurrencyIds(){\n    currencyIdByName = {};\n    currencyIdBySymbol = {};\n    $.get({\n        url: currencyListURL,\n        dataType: \"json\",\n        success: function (data) {\n            data.forEach((element) => {\n                const name = element[\"n\"].toLowerCase();\n                if(!currencyIdByName.hasOwnProperty(name)){\n                    // in case of conflicts, keep the first one as top 250 is first in list\n                    currencyIdByName[name] = element[\"i\"];\n                }\n                const symbol = element[\"s\"].toLowerCase();\n                if(!currencyIdBySymbol.hasOwnProperty(symbol)){\n                    // in case of conflicts, keep the first one as top 250 is first in list\n                    currencyIdBySymbol[symbol] = element[\"i\"];\n                }\n            });\n            currencyDetails = data;\n            fetchedCurrencyIds = true;\n            // refresh images\n            handleDefaultImages();\n        },\n        error: function (result, status) {\n            window.console && console.error(`Impossible to get currency list from coingecko.com: ${result.responseText} (${status})`);\n        }\n    });\n}\n\nfunction handleDefaultImages(){\n    const applyImage = (element, logoUrl) => {\n        if(!element.hasClass(\"default\")){\n            element.attr(\"src\", logoUrl);\n        }\n    }\n    const useLogo = (element, currencyId) => {\n        let logoUrl = currencyLogoById[currencyId]\n        if (logoUrl === null){\n            logoUrl = currencyDefaultImage;\n        }\n        applyImage(element, logoUrl);\n    }\n    const fetchLogos = (currencyIds) => {\n        const successcb = (updated_data, update_url, dom_root_element, msg, status) => {\n            msg.forEach((dataElement) => {\n                currencyLogoById[dataElement.id] = dataElement.logo;\n            })\n            displayImages(false);\n        }\n        const errorcb = (result, status, error) => {\n            window.console && console.error(`Impossible to get currency logos: ${result.responseText} (${status})`);\n        }\n        send_and_interpret_bot_update({currency_ids: [... currencyIds]}, currencyLogoURL,\n            null, successcb, errorcb);\n    }\n    const displayImages = (shouldFetch) => {\n        try {\n            const currencyIds = new Set();\n            $(\".currency-image\").each((_, jselement) => {\n                const element = $(jselement);\n                const imgSrc = element.attr(\"src\");\n                if (imgSrc === \"\" || imgSrc.endsWith(currencyLoadingImageName)) {\n                    if (jselement.hasAttribute(\"data-currency-id\")) {\n                        const currencyId = element.attr(\"data-currency-id\").toLowerCase();\n                        if(currencyLogoById.hasOwnProperty(currencyId)){\n                            useLogo(element, currencyId);\n                        }else{\n                            currencyIds.add(currencyId);\n                        }\n                    } else if (jselement.hasAttribute(\"data-name\")) {\n                        const name = element.attr(\"data-name\").toLowerCase();\n                        if (typeof currencyIdByName === \"undefined\") {\n                            fetchCurrencyIds();\n                        } else if (fetchedCurrencyIds) {\n                            if (currencyIdByName.hasOwnProperty(name)) {\n                                const currencyId = currencyIdByName[name];\n                                if(currencyLogoById.hasOwnProperty(currencyId)){\n                                    useLogo(element, currencyId);\n                                }else{\n                                    currencyIds.add(currencyId);\n                                }\n                            } else {\n                                handleDefaultImage(element, currencyDefaultImage);\n                            }\n                        }\n                    } else if (jselement.hasAttribute(\"data-symbol\")) {\n                        const symbol = element.attr(\"data-symbol\").toLowerCase();\n                        if (typeof currencyIdBySymbol === \"undefined\") {\n                            fetchCurrencyIds();\n                        } else if (fetchedCurrencyIds) {\n                            if (currencyIdBySymbol.hasOwnProperty(symbol)) {\n                                const currencyId = currencyIdBySymbol[symbol];\n                                if(currencyLogoById.hasOwnProperty(currencyId)){\n                                    useLogo(element, currencyId);\n                                }else{\n                                    currencyIds.add(currencyId);\n                                }\n                            } else {\n                                handleDefaultImage(element, currencyDefaultImage);\n                            }\n                        }\n                    }\n                }\n            });\n            if(shouldFetch && currencyIds.size){\n                fetchLogos(currencyIds);\n            }\n        } catch {\n            // fetching currency ids\n        }\n    }\n    displayImages(true);\n}\n\nfunction handle_copy_to_clipboard() {\n    $(\"[data-role=\\\"copy-to-clipboard\\\"]\").on(\"click\", (event) => {\n        const element = $(event.currentTarget);\n        copyToClipBoard(element.data(\"name\"), element.data(\"value\"));\n    })\n}\n\n\n$(document).ready(function() {\n    // register error listeners as soon as possible\n    handleDefaultImages();\n    handle_copy_to_clipboard();\n    $(\".markdown-content\").each(function () {\n        const element = $(this);\n        element.html(markdown_to_html(element.text()));\n    });\n    fetch_images();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/stepper.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction updateProgress(){\n    const progress = getCurrentStepId() * 100 / getStepsCount();\n    $(\".progress-bar\").css('width', progress+'%').attr(\"aria-valuenow\", progress);\n}\n\nfunction triggerCallbacksIfAny(stepId){\n    if (isDefined(stepperCallbackById && isDefined(stepperCallbackById[stepId]))){\n        stepperCallbackById[stepId]();\n    }\n}\n\nfunction updateButtonsDisplay(){\n    const currentStepId = getCurrentStepId();\n    const stepsCount = getStepsCount();\n    const previousButton = $(\"#previous-step\");\n    const nextButton = $(\"#next-step\");\n    if(currentStepId < 2){\n        previousButton.addClass(\"disabled\");\n    }else{\n        previousButton.removeClass(\"disabled\");\n    }\n    if(currentStepId >= stepsCount){\n        nextButton.addClass(\"disabled\");\n    }else{\n        nextButton.removeClass(\"disabled\");\n    }\n}\n\nfunction getStep(stepId){\n    return $(`.tutorial-step[data-step-id=${stepId}]`);\n}\n\nfunction getCurrentStep(){\n    return $(\".tutorial-step\").not(\".d-none\");\n}\n\nfunction getCurrentStepId(){\n    return getCurrentStep().data(\"stepId\");\n}\n\nfunction getStepsCount(){\n    return $(\".tutorial-step\").length;\n}\n\nfunction changeStep(next){\n    const currentStep = getCurrentStepId();\n    const nextStepId = next ? currentStep + 1 : currentStep - 1;\n    if(nextStepId > 0 && nextStepId <= getStepsCount()){\n        getCurrentStep().addClass(hidden_class);\n        getStep(nextStepId).removeClass(hidden_class);\n        window.scrollTo(0, 0);\n    }\n    updateButtonsDisplay();\n    updateProgress();\n    triggerCallbacksIfAny(getCurrentStepId());\n}\n\nfunction handleStepsButtons(){\n    const previousButton = $(\"#previous-step\");\n    const nextButton = $(\"#next-step\");\n    nextButton.click(function (){\n        changeStep(true);\n    });\n    previousButton.click(function (){\n        changeStep(false);\n    });\n}\n\n$(document).ready(function() {\n    updateButtonsDisplay();\n    updateProgress();\n    handleStepsButtons();\n    triggerCallbacksIfAny(getCurrentStepId());\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/tables_display.js",
    "content": "const MAX_PRICE_DIGITS = 8;\nconst _displaySort = (data, type) => {\n    if (type === 'display') {\n        return data.display\n    }\n    return data.sort;\n}\nconst displayTradesTable = (elementId, trades, refMarket, update) => {\n    const table = $(document.getElementById(elementId));\n    const rows = trades.map((element) => {\n        return [\n            element.symbol,\n            element.type,\n            round_digits(element.price, MAX_PRICE_DIGITS),\n            round_digits(element.amount, MAX_PRICE_DIGITS),\n            element.exchange,\n            {display: `${round_digits(element.cost, 5)} ${element.market}`, sort: element.cost},\n            {\n                display: `${element.ref_market_cost === null ? `no ${element.market} price in ${refMarket}` : round_digits(element.ref_market_cost, 5)}`,\n                sort: element.ref_market_cost === null ? 0 : element.ref_market_cost\n            },\n            {display: `${round_digits(element.fee_cost, 5)} ${element.fee_currency}`, sort: element.fee_cost},\n            {display: element.date, sort: element.time},\n            element.id,\n            element.SoR,\n        ]\n    });\n    let previousSearch = undefined;\n    let previousOrder = [[8, \"desc\"]];\n    let addedRows = true;\n    if (update && $.fn.DataTable.isDataTable(`#${elementId}`)) {\n        const previousDataTable = table.DataTable();\n        previousSearch = previousDataTable.search();\n        previousOrder = previousDataTable.order();\n        addedRows = rows.length !== previousDataTable.rows().data().length;\n        previousDataTable.destroy();\n    }\n    table.DataTable({\n        data: rows,\n        columns: [\n            {title: \"Pair\"},\n            {title: \"Type\"},\n            {title: \"Price\"},\n            {title: \"Quantity\"},\n            {title: \"Exchange\"},\n            {title: \"Total\", render: _displaySort},\n            {title: `${refMarket} Total`, render: _displaySort},\n            {title: \"Fee\", render: _displaySort},\n            {title: \"Execution\", render: _displaySort},\n            {title: \"ID\"},\n            {title: \"#\"},\n        ],\n        paging: true,\n        search: {\n            search: previousSearch,\n        },\n        order: previousOrder,\n    });\n    return addedRows;\n}\nconst displayPositionsTable = (elementId, positions, closePositionUrl, update) => {\n    const table = $(document.getElementById(elementId));\n    const rows = positions.map((element) => {\n        return [\n            `${element.side.toUpperCase()} ${element.contract}`,\n            round_digits(element.amount, 5),\n            {display: `${round_digits(element.value, 5)} ${element.market}`, sort: element.value},\n            round_digits(element.entry_price, MAX_PRICE_DIGITS),\n            round_digits(element.liquidation_price, MAX_PRICE_DIGITS),\n            {display: `${round_digits(element.margin, 5)} ${element.market}`, sort: element.margin},\n            {display: `${round_digits(element.unrealized_pnl, 5)} ${element.market}`, sort: element.unrealized_pnl},\n            element.exchange,\n            element.SoR,\n            {symbol: element.symbol, side: element.side},\n        ]\n    });\n    let previousSearch = undefined;\n    let previousOrder = undefined;\n    if (update) {\n        const previousDataTable = table.DataTable();\n        previousSearch = previousDataTable.search();\n        previousOrder = previousDataTable.order();\n        previousDataTable.destroy();\n    }\n    table.DataTable({\n        data: rows,\n        columns: [\n            {title: \"Contract\"},\n            {title: \"Size\"},\n            {title: \"Value\", render: _displaySort},\n            {title: \"Entry price\"},\n            {title: \"Liquidation price\"},\n            {title: \"Position margin\", render: _displaySort},\n            {title: \"Unrealized PNL\", render: _displaySort},\n            {title: \"Exchange\"},\n            {title: \"#\"},\n            {\n                title: \"Close\",\n                render: (data, type) => {\n                    if (type === 'display') {\n                        return `<button type=\"button\" class=\"btn btn-sm btn-outline-danger waves-effect\" \n                                       data-action=\"close_position\" data-position_symbol=${data.symbol} \n                                       data-position_side=\"${data.side}\"\n                                       data-update-url=\"${closePositionUrl}\">\n                                       <i class=\"fas fa-ban\"></i></button>`\n                    }\n                    return data;\n                },\n            },\n        ],\n        paging: false,\n        search: {\n            search: previousSearch,\n        },\n        order: previousOrder,\n    });\n}\nconst displayOrdersTable = (elementId, orders, cancelOrderUrl, update) => {\n    const table = $(document.getElementById(elementId));\n    const canCancelOrders = cancelOrderUrl !== undefined;\n    const rows = orders.map((element) => {\n        const row = [\n            element.symbol,\n            element.type,\n            round_digits(element.price, MAX_PRICE_DIGITS),\n            round_digits(element.amount, MAX_PRICE_DIGITS),\n            element.exchange,\n            {display: element.date, sort: element.time},\n            {display: `${round_digits(element.cost, MAX_PRICE_DIGITS)} ${element.market}`, sort: element.cost},\n            element.SoR,\n            element.id,\n        ]\n        if (canCancelOrders){\n            row.push(element.id)\n        }\n        return row\n    });\n    let previousOrder = [[5, \"desc\"]];\n    if(update){\n        const previousDataTable = table.DataTable();\n        previousOrder = previousDataTable.order();\n        previousDataTable.destroy();\n    }\n    const columns = [\n        { title: \"Pair\" },\n        { title: \"Type\" },\n        { title: \"Price\" },\n        { title: \"Quantity\" },\n        { title: \"Exchange\" },\n        { title: \"Date\", render: _displaySort },\n        { title: \"Total\", render: _displaySort },\n        { title: \"#\" },\n    ]\n    if (canCancelOrders) {\n        columns.push({\n            title: \"Cancel\",\n            render: (data, type) => {\n                if (type === 'display') {\n                   return `<button type=\"button\" class=\"btn btn-sm btn-outline-danger waves-effect\" \n                                   action=\"cancel_order\" order_desc=\"${ data }\" \n                                   update-url=\"${cancelOrderUrl}\">\n                                   <i class=\"fas fa-ban\"></i></button>`\n                }\n                return data;\n            },\n        });\n    }\n    table.DataTable({\n        data: rows,\n        columns: columns,\n        paging: false,\n        order: previousOrder,\n    });\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/tracking.js",
    "content": "function posthog_loaded(posthog) {\n\n    const getUserEmail = () => {\n        return getUserDetails().email || \"\";\n    }\n\n    const getUserDetails = () => {\n        if (_USER_DETAILS.email === \"\"){\n            // do not erase email if unset\n            delete _USER_DETAILS.email;\n        }\n        return _USER_DETAILS\n    }\n\n    const updateUserDetails = () => {\n        posthog.capture(\n            'up_user_details',\n            properties={\n                '$set': getUserDetails(),\n            }\n        )\n    }\n\n    const shouldUpdateUserDetails = () => {\n        const currentProperties = posthog.get_property('$stored_person_properties');\n        if(currentProperties === undefined){\n            return true;\n        }\n        if(isDefined(currentProperties)){\n            const currentDetails = getUserDetails();\n            if(currentDetails.email === undefined){\n                // compare without email (otherwise result is always different as no email is currently set)\n                const localProperties = JSON.parse(JSON.stringify(currentProperties));\n                delete localProperties.email\n                return JSON.stringify(localProperties) !== JSON.stringify(getUserDetails());\n            }\n        }\n        return  JSON.stringify(currentProperties) !== JSON.stringify(getUserDetails());\n    }\n\n    const shouldReset = (newEmail) => {\n        const previousId = posthog.get_distinct_id();\n        return (\n            newEmail !== previousId\n            // if @ is the user id, it's an email which is different from the current one: this is a new user\n            && previousId.indexOf(\"@\") !== -1\n        );\n    }\n\n    const identify = (email) => {\n        posthog.identify(\n            email,\n            getUserDetails() // optional: set additional person properties\n        );\n    }\n\n    const updateUserIfNecessary = () => {\n        if (!_IS_ALLOWING_TRACKING){\n            // tracking disabled\n            return\n        }\n        const email = getUserEmail();\n        if (email !== \"\" && posthog.get_distinct_id() !== email){\n            if (shouldReset(email)){\n                // If you also want to reset the device_id so that the device will be considered a new device in\n                // future events, you can pass true as an argument\n                // => past events will be bound to the current user as soon as he connects but avoid binding later events\n                // in case the user changes\n                console.log(\"PH: Resetting user\")\n                const resetDeviceId = true\n                posthog.reset(resetDeviceId);\n            }\n            // new authenticated email: identify\n            console.log(\"PH: Identifying user\")\n            identify(email);\n        }else{\n            if (shouldUpdateUserDetails()){\n                console.log(\"PH: updating user details\")\n                updateUserDetails();\n            }\n        }\n    }\n\n    updateUserIfNecessary();\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/tutorial.js",
    "content": "function getWebsiteLink(route, name) {\n    return `<a class=\"\" target=\"_blank\" rel=\"noopener\" href=\"${getWebsiteUrl()}${route}\">${name}</a>`\n}\n\nfunction getDocsLink(route, name) {\n    return `<a class=\"\" target=\"_blank\" rel=\"noopener\" href=\"${getDocsUrl()}${route}\">${name}</a>`\n}\n\nfunction getExchangesDocsLink(route, name) {\n    return `<a class=\"\" target=\"_blank\" rel=\"noopener\" href=\"${getExchangesDocsUrl()}${route}\">${name}</a>`\n}\n\n_TUTORIALS = {\n    home: () => {\n        let profileName = \"selected\";\n        if($(`span[data-selected-profile]`).length){\n            profileName = $(`span[data-selected-profile]`).data(\"selected-profile\");\n        }\n        return {\n            steps: [\n                {\n                    title: 'Welcome to OctoBot',\n                    intro: `Your OctoBot is now trading using the ${profileName} profile.`\n                },\n                {\n                    title: 'Quickly navigate through your OctoBot',\n                    element: document.querySelector('#main-nav-bar'),\n                    intro: ''\n                },\n                {\n                    title: 'Your live OctoBot',\n                    element: document.querySelector('#main-nav-left-part'),\n                    intro: 'See and configure your live OctoBot.'\n                },\n                {\n                    title: 'Trading activity',\n                    element: document.querySelector('#main-nav-trading'),\n                    intro: `View your OctoBot's current open orders and trades history.`\n                },\n                {\n                    title: 'Portfolio',\n                    element: document.querySelector('#main-nav-portfolio'),\n                    intro: `Quickly checkout your funds at any given time, on every exchange.`\n                },\n                {\n                    title: 'Profile',\n                    element: document.querySelector('#main-nav-profile'),\n                    intro: `Change any setting about your profile (traded cryptocurrencies, exchanges, strategies, ...).`\n                },\n                {\n                    title: 'Trading type',\n                    element: document.querySelector('#main-nav-trading-type'),\n                    intro: 'See if your Octobot is trading simulated or real funds.'\n                },\n                {\n                    title: 'Test your profile',\n                    element: document.querySelector('#main-nav-backtesting'),\n                    intro: 'Backtest your current configuration using historical data.'\n                },\n                {\n                    title: 'Community',\n                    element: document.querySelector('#main-nav-community'),\n                    intro: 'Access OctoBot cloud strategies, your OctoBot account and the community stats.'\n                },\n                {\n                    title: 'Customize your dashboard',\n                    element: document.querySelector('#all-watched-markets'),\n                    intro: 'Add watched markets from the Trading tab.'\n                },\n                {\n                    title: \"That's it !\",\n                    intro: 'We hope you will enjoy OctoBot. Use the <a class=\"blue-text\" target=\"_blank\"><i class=\"fa-solid fa-question\"></i></a> buttons to learn more on how to use OctoBot'\n                },\n            ]\n        }\n    },\n\n    profile: () => {\n        return {\n            steps: [\n                {\n                    title: 'Profile configuration',\n                    intro: 'From this tab, you can configure your OctoBot profile.'\n                },\n                {\n                    title: 'Select another profile',\n                    element: document.querySelector('#profile-selector-link'),\n                    intro: 'You can change the profile used by your OctoBot at any time.'\n                },\n                {\n                    title: 'Customize your profiles',\n                    element: document.querySelector('#edit-profiles-button'),\n                    intro: 'You can create you own profiles based on existing ones.'\n                },\n                {\n                    title: 'Set your profile strategy',\n                    element: document.querySelector('#panelStrategies-tab'),\n                    intro: \"Select and configure your current profile's trading mode and configuration.\"\n                },\n                {\n                    title: 'Select traded cryptocurrencies',\n                    element: document.querySelector('#panelCurrency-tab'),\n                    intro: \"Select the cryptocurrencies to trade on your current profile.\"\n                },\n                {\n                    title: 'Select exchanges',\n                    element: document.querySelector('#panelExchanges-tab'),\n                    intro: \"Select the exchange(s) to trade on with your current profile.\"\n                },\n                {\n                    title: 'Select trading configuration',\n                    element: document.querySelector('#panelTrading-tab'),\n                    intro: \"Select whether to trade using simulated funds or your real funds on exchanges.\"\n                },\n                {\n                    title: 'Save your changes',\n                    element: document.querySelector('#save-config'),\n                    intro: \"When configuring your profile, changes saved when you hit 'save'.\"\n                },\n                {\n                    title: 'See also',\n                    intro: `More details on ${getDocsLink(\"/octobot-configuration/profiles?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=profiles_intro\", \"the profiles guide\")}.`\n                },\n            ]\n        }\n    },\n\n    profile_selector: () => {\n        return {\n            steps: [\n                {\n                    title: 'Welcome to OctoBot',\n                    intro: `To start with OctoBot, select the trading profile that you want to use at first.`\n                },\n                {\n                    title: 'Choosing your profile',\n                    element: document.querySelector('[data-target=\"#defaultModal\"]'),\n                    intro: `Find more details on each profile using the details button.`\n                },\n                {\n                    title: 'Select your profile',\n                    element: document.querySelector('.activate-profile-button'),\n                    intro: `Once you found the right profile, just activate it.`\n                },\n                {\n                    title: 'Get more profiles',\n                    element: document.querySelector('.login_box'),\n                    intro: `Use OctoBot cloud to add profiles to your OctoBot.`\n                },\n            ]\n        }\n    },\n\n    automations: () => {\n        return {\n            steps: [\n                {\n                    title: 'Welcome to automations',\n                    intro: `Here you can automate any action directly form your OctoBot.`\n                },\n                {\n                    title: 'What are automations ?',\n                    element: document.querySelector('#configEditor'),\n                    intro: `Automations are actions your OctoBot can process on a given event or frequency.`\n                },\n                {\n                    title: 'Example 1/2',\n                    element: document.querySelector('#configEditor'),\n                    intro: `Make your OctoBot send you a notification if your profitability increased by 10% in a day.`\n                },\n                {\n                    title: 'Example 2/2',\n                    element: document.querySelector('#configEditor'),\n                    intro: `Cancel all open orders if the price of BTC/USDT crosses 70.000 USDT.`\n                },\n                {\n                    title: 'Launch automations',\n                    element: document.querySelector('#applyAutomations'),\n                    intro: `Automations are started with your OctoBot and when hitting the Apply button.`\n                },\n                {\n                    title: 'Automations are saved in your profile',\n                    element: document.querySelector('#page-title'),\n                    intro: `You can quickly switch automations by switching profiles.`\n                },\n                {\n                    title: 'Share automations',\n                    element: document.querySelector('#page-title'),\n                    intro: `As they are linked to a profile, you can share them with your profile.`\n                },\n            ]\n        }\n    },\n\n    profitability: () => {\n        return {\n            steps: [\n                {\n                    title: 'Your profitability',\n                    element: document.querySelector('#profitability-display'),\n                    intro: 'Your OctoBot trading profitability compared to the market.'\n                },\n                {\n                    title: 'See also',\n                    intro: `More details on ${getDocsLink(\"/octobot-usage/understanding-profitability?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=dashboard_intro\", \"the OctoBot docs\")}.`\n                },\n            ]\n        }\n    },\n\n    \"mm:home\": () => {\n        return {\n            steps: [\n                {\n                    title: 'Welcome to OctoBot Market Making',\n                    intro: 'This free software lets you easily automate market making strategies.'\n                },\n                {\n                    title: 'This is your dashboard',\n                    element: document.querySelector('#dashboard-graph'),\n                    intro: `From this graph, you can follow your market price, market making orders and trades.`\n                },\n                {\n                    title: 'Simulated trading',\n                    element: document.querySelector('#trading-type-indicator'),\n                    intro: `This part shows if your bot is currently using virtual funds (simulated trading) or trades with a real exchange account.`\n                },\n                {\n                    title: 'Your open orders',\n                    element: document.querySelector('#openOrderTable'),\n                    intro: `In this table are displayed details about your strategy current open orders.`\n                },\n                {\n                    title: 'Your account balance',\n                    element: document.querySelector('#profitability-display'),\n                    intro: `Here will be displayed the chart of your historical balance, once your bot will have run for some time.`\n                },\n                {\n                    title: 'Your trades',\n                    element: document.querySelector('#trades-table'),\n                    intro: `Your market making trade history will be detailed on this table.`\n                },\n                {\n                    title: \"That's it !\",\n                    intro: 'Thank your for using OctoBot Market Making.'\n                },\n            ]\n        }\n    },\n\n    \"mm:configuration\": () => {\n        return {\n            steps: [\n                {\n                    title: 'Configuration',\n                    intro: 'This page lets you configure your strategy.'\n                },\n                {\n                    title: 'Exchange and pair',\n                    element: document.querySelector('#exchange-and-pair'),\n                    intro: `Select the exchange and trading pair to provide liquidity on.`\n                },\n                {\n                    title: 'Exchanges configuration',\n                    element: document.querySelector('#exchange-configuration'),\n                    intro: `You can enter your target exchange and API Keys here.`\n                },\n                {\n                    title: 'Simulated trading',\n                    element: document.querySelector('#trading-simulation'),\n                    intro: `Use the risk-free trading simulator to fine tune your configuration before using real funds.`\n                },\n                {\n                    title: 'Strategy details',\n                    element: document.querySelector('#trading-mode-config-editor'),\n                    intro: `Edit your strategy details to create the strategy of your choice.`\n                },\n            ]\n        }\n    },\n\n    account_exchanges: () => {\n        return {\n            steps: [\n                {\n                    title: 'Adding exchanges',\n                    element: document.querySelector('#new-exchange-selector'),\n                    intro: 'Add as many exchanges as you like. You can enable or disable them in each profile.'\n                },\n                {\n                    title: 'Exchanges configuration',\n                    intro: 'Exchange configurations are only required to trade with real funds on the exchange.'\n                },\n                {\n                    title: 'See also',\n                    intro: `More details on supported exchanges in the ${getExchangesDocsLink(\"?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_config\", \"OctoBot exchanges docs\")}.`\n                },\n            ]\n        }\n    },\n\n    backtesting: () => {\n        return {\n            steps: [\n                {\n                    title: 'Backtesting',\n                    intro: 'Test your current profile using historical data.'\n                },\n                {\n                    title: 'Get historical',\n                    element: document.querySelector('#data-collector-link'),\n                    intro: 'Download historical market data to test your profiles on.'\n                },\n                {\n                    title: 'See also',\n                    intro: `More details the ${getDocsLink(\"/octobot-usage/backtesting?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=backtesting_intro\", \"backtesting guide\")}.`\n                },\n            ]\n        }\n    },\n}\n\nfunction registerTutorial(tutorialName, callback){\n    _TUTORIALS[tutorialName] = callback\n}\n\nfunction displayLocalTutorial(tutorialName, afterExitCallback){\n    if(typeof _TUTORIALS[tutorialName] === \"undefined\"){\n        console.error(`Tutorial not found ${tutorialName}`)\n        return;\n    }\n    const defaultOptions = {\n        disableInteraction: true,\n        showProgress: true,\n        showBullets: false,\n    }\n    const intro = introJs().setOptions(defaultOptions).setOptions(_TUTORIALS[tutorialName]());\n    if(afterExitCallback !== null){\n        intro.onexit(afterExitCallback);\n    }\n    intro.start();\n}\n\nfunction startTutorialIfNecessary(tutorialName, afterExitCallback=null) {\n    if($(`span[data-display-intro=\"True\"]`).length === 0){\n        return false;\n    }\n    displayLocalTutorial(tutorialName, afterExitCallback);\n    return true;\n}\n\n\n$(document).ready(function () {\n   $(`a[data-intro]`).each((_, element) => {\n       $(element).on(\"click\", (event) => {\n           displayLocalTutorial($(event.currentTarget).data(\"intro\"), null)\n       })\n   })\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/common/util.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction get_websocket(namespace){\n    // Connect to the Socket.IO server.\n    // The connection URL has the following format, relative to the current page:\n    //     http[s]://<domain>:<port>[/<namespace>]\n    return io(\n        namespace,\n        {\n            reconnectionDelay: 2000, // Prevent unexpected disconnection on slow loading pages (ex: first config load)\n            transports: [\"polling\", \"websocket\"], // update polling to ws when possible\n        });\n}\n\nfunction getAudioMediaUrl(mediaName){\n    const baseUrl = $(\"#resources-urls\").data(\"audio-media-url\")\n    return `${baseUrl}${mediaName}`\n}\n\nfunction setup_editable(){\n    $.fn.editable.defaults.mode = 'inline';\n}\n\nfunction get_color(index){\n    let color_index = index % (material_colors.length);\n    return material_colors[color_index];\n}\n\nfunction get_dark_color(index){\n    let color_index = index % (material_dark_colors.length);\n    return material_dark_colors[color_index];\n}\n\nfunction handle_editable(){\n    const elements = []\n    $(\".editable\").each(function(){\n        elements.push($(this).editable());\n    });\n    return elements\n}\n\nfunction hide_editables(elements){\n    elements.forEach((element) => {\n        element.editable('hide');\n        // element.destroy();\n    })\n}\n\nfunction trigger_file_downloader_on_click(element){\n    if(element.length){\n        element.click(function (){\n            window.window.location  = $(this).attr(\"data-url\");\n        });\n    }\n}\n\nfunction replace_break_line(str, replacement=\"\"){\n    return str.replace(/(?:\\r\\n|\\r|\\n)/g, replacement);\n}\n\nfunction replace_spaces(str, replacement=\"\"){\n    return str.replace(/ /g, replacement);\n}\n\nfunction get_selected_options(element){\n    const selected_options = [];\n    element.find(\":selected\").each(function(){\n        selected_options.push($(this).val());\n    });\n    return selected_options;\n}\n\n\n// utility functions\nfunction isDefined(thing){\n    return (typeof thing !== \"undefined\" && thing !== false && thing !==null);\n}\n\nfunction log(...texts){\n    if(window.console){\n        console.log(...texts);\n    }\n}\n\nfunction get_events(elem, event_type){\n    const events = $._data( elem[0], 'events' );\n    if(typeof events === \"undefined\"){\n        return [];\n    }\n    return $._data( elem[0], 'events' )[event_type];\n}\n\nfunction add_event_if_not_already_added(elem, event_type, handler){\n    if(!check_has_event_using_handler(elem, event_type, handler)){\n        elem.on(event_type, handler);\n    }\n}\n\nfunction updateProgressBar(elementId, progress){\n    $(document.getElementById(elementId)).css('width', progress+'%').attr(\"aria-valuenow\", progress);\n}\n\nfunction check_has_event_using_handler(elem, event_type, handler){\n    const events = get_events(elem, event_type);\n    let has_events = false;\n    $.each(events, function () {\n        if($(this)[0][\"handler\"] === handler){\n            has_events = true;\n        }\n    });\n    return has_events;\n}\n\nfunction generic_request_success_callback(updated_data, update_url, dom_root_element, msg, status) {\n    if(msg.hasOwnProperty(\"title\")){\n        create_alert(\"success\", msg[\"title\"], msg[\"details\"]);\n    }else{\n        create_alert(\"success\", msg, \"\");\n    }\n}\n\nfunction generic_request_failure_callback(updated_data, update_url, dom_root_element, msg, status) {\n    if(isBotDisconnected()){\n        create_alert(\"error\", \"Can't connect to OctoBot\", \"Your OctoBot might be offline.\");\n    }else{\n        create_alert(\"error\", msg.responseText, \"\");\n    }\n}\n\nfunction isMobileDisplay() {\n    return $(window).width() < mobile_width_breakpoint;\n}\n\nfunction isMediumDisplay() {\n    return $(window).width() < medium_width_breakpoint;\n}\n\nconst getTextColor = () => {\n    return getComputedStyle(document.body).getPropertyValue('--mdb-primary-text-emphasis')\n}\n\nconst getTextColorRGB = () => {\n    return getComputedStyle(document.body).getPropertyValue('--mdb-emphasis-color-rgb')\n}\n\nconst isDarkTheme = () => {\n    return $(\"html\").data(\"mdb-theme\") === \"dark\"\n}\n\nconst handle_rounded_numbers_display = () => {\n    $(\".rounded-number\").each(function (){\n        const text = $(this).text().trim();\n        if (!isNaN(text)){\n            const value = Number(text);\n            const decimal = value > 1 ? 3 : 8;\n            $(this).text(handle_numbers(round_digits(text, decimal)));\n        }\n    });\n}\n\nfunction round_digits(number, decimals) {\n    const rounded = Number(Math.round(`${number}e${decimals}`) + `e-${decimals}`);\n    if(isNaN(rounded)){\n        const n = Number(`${number}`);\n        return n.toFixed(decimals);\n    }\n    return rounded;\n}\n\nfunction handle_numbers(number) {\n    let regEx2 = /[0]+$/;\n    let regEx3 = /[.]$/;\n    const numb_repr = Number(number);\n    const numb_str = numb_repr.toString();\n    let numb_digits = numb_str.length;\n    const exp_index = numb_str.indexOf('e-');\n    if (exp_index > -1){\n        let decimals = 0;\n        if (numb_str.indexOf('.') > -1) {\n            decimals = numb_str.substr(0, exp_index).split(\".\")[1].length;\n        }\n        numb_digits = Number(numb_str.split(\"e-\")[1]) + decimals;\n    }\n    let numb = numb_repr.toFixed(numb_digits);\n\n    if (numb.indexOf('.')>-1){\n        numb = numb.replace(regEx2,'');  // Remove trailing 0's\n    }\n    return numb.replace(regEx3,'');  // Remove trailing decimal\n}\n\nfunction fix_config_values(config, schema){\n    ensure_all_config_values(config, schema);\n    $.each(config, function (key, val) {\n        if(typeof val === \"number\"){\n            config[key] = handle_numbers(val);\n        }else if (val instanceof Object){\n            fix_config_values(config[key], undefined);\n        }\n    });\n}\n\nconst ensure_all_config_values = (config, schema) => {\n    if(!isDefined(schema) || typeof schema.properties === \"undefined\"){\n        return\n    }\n    // ensure each schema element has a value or there might be display issues\n    Object.keys(schema.properties).forEach(key => {\n        if(typeof config[key] === \"undefined\"){\n            config[key] = schema.properties[key].default;\n        }\n    })\n}\n\nfunction getValueChangedFromRef(newObject, refObject, allowUndefinedValues=true) {\n    let changes = false;\n    if (newObject instanceof Array && newObject.length !== refObject.length){\n        changes = true;\n    }\n    else{\n        Object.keys(newObject).forEach((key) => {\n            if(changes){\n                return;\n            }\n            const val = newObject[key];\n            const refVal = refObject[key];\n            if (allowUndefinedValues && (typeof refVal === \"undefined\" || typeof val === \"undefined\")){\n                // ignore missing values\n                return;\n            }\n            else if (val instanceof Array || val instanceof Object){\n                changes = getValueChangedFromRef(val, refVal, allowUndefinedValues);\n            }\n            else if (refObject[key] !== val){\n                if (typeof val === \"number\"){\n                    changes = Number(refVal) !== val;\n                }else{\n                    changes = true;\n                }\n            }\n            if (changes){\n                return;\n            }\n        });\n    }\n    return changes;\n}\n\nfunction historyGoBack() {\n    window.history.back();\n}\n\nfunction showModalIfAny(element){\n    if(element){\n        element.modal();\n    }\n}\n\nfunction hideModalIfAny(element){\n    if(element){\n        element.modal(\"hide\");\n    }\n}\n\n// Inspired from https://davidwalsh.name/javascript-debounce-function\n// Function, that, as long as it continues to be invoked, will not\n// be triggered. The function will be called after it stops being called for\n// N milliseconds. If `immediate` is passed, trigger the function on the\n// leading edge, instead of the trailing.\nconst debounce = (func, wait, immediate) => {\n\tlet debounceTimeout;\n\treturn () => {\n        const context = this;\n        const later = () => {\n            debounceTimeout = null;\n            if (!immediate) {\n                func.apply(context);\n            }\n        };\n        const callNow = immediate && !debounceTimeout;\n        clearTimeout(debounceTimeout);\n        debounceTimeout = setTimeout(later, wait);\n        if (callNow) {\n            func.apply(context);\n        }\n    }\n}\n\nfunction unique(array){\n    return $.grep(array, function(el, index) {\n        return index === $.inArray(el, array);\n    });\n}\n\nfunction download_data(data, filename, content_type=\"application/json\"){\n    let a = window.document.createElement('a');\n    a.href = window.URL.createObjectURL(new Blob([data], {type: content_type}));\n    a.download = filename;\n\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n}\n\nfunction display_generic_modal(title, content, warning, yes_button_callback, no_button_callback){\n    let generic_modal = $(\"#genericModal\");\n    $(\"#genericModalTitle\").text(title);\n    $(\"#genericModalContent\").text(content);\n    if(warning !== \"\"){\n        $(\"#genericModalWarning\").removeClass(hidden_class);\n        $(\"#genericModalWarningMessage\").text(warning);\n    }\n    $(\"#genericModalButtonYes\").on(\"click\", function() {\n        yes_button_callback();\n        hideModalIfAny(generic_modal);\n    });\n    $(\"#genericModalButtonNo\").on(\"click\", function(){\n        if(no_button_callback !== null){\n            no_button_callback();\n        }\n        hideModalIfAny(generic_modal);\n    });\n\n    showModalIfAny(generic_modal);\n    return generic_modal;\n}\n\nfunction updateInputIfValue(elementId, config, configKey, elementType){\n    const value = config[configKey];\n    if(typeof value !== \"undefined\" && value !== null && value !== \"\"){\n        const element = $(document.getElementById(elementId));\n        if(element.length){\n            if(elementType === \"date\") {\n                element.val(new Date(config[configKey]).toISOString().split('T')[0].slice(0, 10))\n            } else if(elementType === \"bool\"){\n                element.prop(\"checked\", config[configKey]);\n            }\n            else {\n                element.val(config[configKey]);\n            }\n        }\n    }\n}\n\nfunction randomizeArray(array) {\n    array.sort(() => Math.random() - 0.5);\n}\n\nfunction validateJSONEditor(editor) {\n    const errors = editor.validate();\n    let errorsDesc = \"\";\n    if(errors.length) {\n        window.console&&console.error(\"Errors when validating editor:\", errors);\n        errors.map((error) => {\n            errorsDesc = `${errorsDesc}${error.path.split(\"root.\")[1]} ${error.message}\\n`\n        })\n    }\n    return errorsDesc;\n}\n\nfunction getWebsiteUrl() {\n    return $(\"#global-urls\").data(\"website-url\");\n}\n\nfunction getDocsUrl() {\n    return $(\"#global-urls\").data(\"docs-url\");\n}\n\nfunction getExchangesDocsUrl() {\n    return $(\"#global-urls\").data(\"exchanges-docs-url\");\n}\n\nfunction paginatedSelect2(selectElement, options, pageSize){\n    // WIP: issue: focus not working on options\n    jQuery.fn.select2.amd.require(\n        [\"select2/data/array\", \"select2/utils\"],\n        (ArrayData, Utils) => {\n            function CustomData($element, options) {\n                CustomData.__super__.constructor.call(this, $element, options);\n            }\n\n            Utils.Extend(CustomData, ArrayData);\n\n            CustomData.prototype.query = function (params, callback) {\n                let results = [];\n                if (typeof params.term !== \"undefined\" && params.term !== '') {\n                    const toSearch = params.term.toUpperCase()\n                    results = options.filter((option) => {\n                        return option.text.toUpperCase().indexOf(toSearch) !== -1;\n                    });\n                } else {\n                    results = options;\n                }\n\n                if (!(\"page\" in params)) {\n                    params.page = 1;\n                }\n                const data = {};\n                data.results = results.slice((params.page - 1) * pageSize, params.page * pageSize);\n                data.pagination = {};\n                data.pagination.more = params.page * pageSize < results.length;\n                callback(data);\n            };\n            // add select2 selector\n            selectElement.select2({\n                ajax: {},\n                width: '200', // need to override the changed default\n                tags: true,\n                dataAdapter: CustomData\n            });\n        }\n    );\n}\n\nconst sortTimeFrames = (timeFrames) => {\n    timeFrames.sort((a, b) => TimeFramesMinutes[a] - TimeFramesMinutes[b]);\n    return timeFrames;\n}\n\nfunction activate_tab(tabElement, nestedNavBar=undefined){\n    if(!tabElement.hasClass(\"active\")){\n        if(typeof nestedNavBar !== \"undefined\"){\n            // manually handle sidebar navigation to work with nested elements\n            nestedNavBar.each(function (){\n                $(this).removeClass(\"active\");\n            })\n        }\n        tabElement.tab('show');\n    }\n}\n\n\nfunction selectFirstTab(nestedNavBar=undefined){\n    let activatedTab = false;\n    const anchor = $(location).attr('hash');\n    if (anchor){\n        const tab = $(`${anchor}-tab`);\n        if (typeof tab !== \"undefined\") {\n            activate_tab(tab, nestedNavBar);\n            activatedTab = true;\n        }\n    }\n    if (!activatedTab){\n        activate_tab($(\"[data-tab='default']\"), nestedNavBar);\n    }\n}\n\nfunction copyToClipBoard(name, value) {\n    if(!navigator.clipboard){\n        create_alert(\n            \"error\",\n            \"Browser security is preventing copy. Please manually copy this value\"\n        );\n    }\n    navigator.clipboard.writeText(value);\n    create_alert(\"success\", `${name} copied to clipboard`);\n}\n\nasync function sleep(milliseconds) {\n    return new Promise(r => setTimeout(r, milliseconds))\n}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/advanced_matrix.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n function init_select_filter(){\n    let evaluators_array = [];\n    let timeframes_array = [];\n    let symbols_array = [];\n    let exchanges_array = [];\n    $.each(matrix_table.rows().data(), function(index, data) {\n        evaluators_array.push(data[matrix_table_evaluator_index]);\n        timeframes_array.push(data[matrix_table_timeframe_index]);\n        symbols_array.push(data[matrix_table_symbol_index]);\n        exchanges_array.push(data[matrix_table_exchange_index]);\n    });\n    evaluators_array = unique(evaluators_array);\n    timeframes_array = unique(timeframes_array);\n    symbols_array = unique(symbols_array);\n    exchanges_array = unique(exchanges_array);\n\n    let evaluators_select = $(\"#evaluatorsSelect\").select2({\n        closeOnSelect: false,\n        placeholder: \"Evaluators\"\n    });\n    $.each(evaluators_array, function(index, value) {\n        evaluators_select[0].add(new Option(value,value));\n    });\n    evaluators_select.on('change', function(){\n        evaluators_selected = evaluators_select.val();\n        matrix_table.columns(matrix_table_evaluator_index).search(\n            evaluators_selected.length ? ('^(' + evaluators_selected.join(\"|\") + ')$') : '', true, false\n        ).draw();\n    });\n\n    let timeframes_select = $(\"#timeframesSelect\").select2({\n        closeOnSelect: false,\n        placeholder: \"Timeframes\"\n    });\n    $.each(timeframes_array, function(index, value) {\n        timeframes_select[0].add(new Option(value,value));\n    });\n    timeframes_select.on('change', function(){\n        timeframes_selected = timeframes_select.val();\n        matrix_table.columns(matrix_table_timeframe_index).search(\n            timeframes_selected.length ? ('^(' + timeframes_selected.join(\"|\") + ')$') : '', true, false\n        ).draw();\n    });\n\n    let symbols_select = $(\"#symbolsSelect\").select2({\n        closeOnSelect: false,\n        placeholder: \"Symbols\"\n    });\n    $.each(symbols_array, function(index, value) {\n        symbols_select[0].add(new Option(value,value));\n    });\n    symbols_select.on('change', function(){\n        symbols_selected = symbols_select.val();\n        matrix_table.columns(matrix_table_symbol_index).search(\n            symbols_selected.length ? ('^(' + symbols_selected.join(\"|\") + ')$') : '', true, false\n        ).draw();\n    });\n\n    let exchanges_select = $(\"#exchangesSelect\").select2({\n        closeOnSelect: false,\n        placeholder: \"Exchanges\"\n    });\n    $.each(exchanges_array, function(index, value) {\n        exchanges_select[0].add(new Option(value,value));\n    });\n    exchanges_select.on('change', function(){\n        exchanges_selected = exchanges_select.val();\n        matrix_table.columns(matrix_table_exchange_index).search(\n            exchanges_selected.length ? ('^(' + exchanges_selected.join(\"|\") + ')$') : '', true, false\n        ).draw();\n    });\n}\nconst matrix_table = $('#matrixDataTable').DataTable();\n\nconst matrix_table_evaluator_index = 0;\nconst matrix_table_timeframe_index = 2;\nconst matrix_table_symbol_index = 3;\nconst matrix_table_exchange_index = 4;\n\n$(document).ready(function() {\n    init_select_filter();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/automations.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    const displayFeedbackFormIfNecessary = () => {\n        const feedbackFormData = $(\"#feedback-form-data\");\n        if(feedbackFormData.data(\"display-form\") === \"True\") {\n            displayFeedbackForm(\n                feedbackFormData.data(\"form-to-display\"),\n                feedbackFormData.data(\"user-id\"),\n                feedbackFormData.data(\"on-submit-url\"),\n            );\n        }\n    };\n    const onEditorChange = (newValue) => {\n        const update_url = $(\"button[data-role='saveConfig']\").attr(update_url_attr);\n        updateTentacleConfig(newValue, update_url);\n    };\n    const startAutomations = () => {\n        const successCallback = (updated_data, update_url, dom_root_element, msg, status) => {\n            create_alert(\"success\", \"Automations started\");\n        }\n        const update_url = $(\"button[data-role='startAutomations']\").attr(update_url_attr);\n        send_and_interpret_bot_update(null, update_url, null, successCallback);\n    }\n    const updateAutomationsCount = (delta) => {\n        if(configEditor === null){\n            return;\n        }\n        const automationsCount = configEditor.getEditor(\"root.automations_count\");\n        const updatedValue = Number(automationsCount.getValue()) + delta;\n        if(updatedValue < 0){\n            return;\n        }\n        automationsCount.setValue(String(updatedValue));\n    }\n    const addAutomation = () => {\n        updateAutomationsCount(1);\n    }\n    const removeAutomation = () => {\n        updateAutomationsCount(-1);\n    }\n    if (!startTutorialIfNecessary(\"automations\")){\n        displayFeedbackFormIfNecessary();\n    }\n    addEditorChangeEventCallback(onEditorChange);\n    $(\"button[data-role='startAutomations']\").on(\"click\", startAutomations);\n    $(\"button[data-role='add-automation']\").on(\"click\", addAutomation);\n    $(\"button[data-role='remove-automation']\").on(\"click\", removeAutomation);\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/backtesting.js",
    "content": "\n/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\nfunction get_selected_files(){\n    const selected_files = [];\n    const selectedRows = dataFilesTable.rows(\n        function ( idx, data, node ) {\n            return $(node).find(\"input[type='checkbox']:checked\").length > 0;\n        }\n    ).eq(0);\n    if(selectedRows){\n        selectedRows.each(function( index ) {\n            selected_files.push(dataFilesTable.row( index ).data()[6]);\n        });\n    }\n    return selected_files;\n}\n\nfunction handle_backtesting_buttons(){\n    $(\"#startBacktesting\").click(function(){\n        $(\"#backtesting_progress_bar\").show();\n        lock_interface();\n        const request = {};\n        request[\"files\"] = get_selected_files();\n        if(check_date_range_available()){\n            if(!check_date_range()){\n                create_alert(\"error\", \"Invalid date range.\", \"\");\n                return;\n            }\n            request[\"start_timestamp\"] = startDate.val().length ? (new Date(startDate.val()).getTime()) : null;\n            request[\"end_timestamp\"] = endDate.val().length ? (new Date(endDate.val()).getTime()) : null;\n        }\n        const update_url = $(\"#startBacktesting\").attr(\"start-url\");\n        const run_on_common_part_only = syncDataOnlyCheckbox.is(\":checked\");\n        start_backtesting(request, `${update_url}&run_on_common_part_only=${run_on_common_part_only}`);\n    });\n}\n\nfunction handle_file_selection(){\n    const selectable_datafile = $(\".selectable_datafile\");\n    selectable_datafile.unbind('click');\n    selectable_datafile.click(function () {\n        const row_element = $(this);\n        if (row_element.hasClass(selected_item_class)){\n            row_element.removeClass(selected_item_class);\n            row_element.find(\".dataFileCheckbox\").prop('checked', false);\n        }else{\n            row_element.toggleClass(selected_item_class);\n            const checkbox = row_element.find(\".dataFileCheckbox\");\n            const symbols = checkbox.attr(\"symbols\");\n            const data_file = checkbox.attr(\"data-file\");\n            checkbox.prop('checked', true);\n            // uncheck same symbols from other rows if any\n            $(\"#dataFilesTable\").find(\"input[type='checkbox']:checked\").each(function(){\n                if($(this).attr(\"symbols\") === symbols && !($(this).attr(\"data-file\") === data_file)){\n                    $(this).closest('tr').removeClass(selected_item_class);\n                    $(this).prop('checked', false);\n                }\n            });\n        }\n        if($(\"#dataFilesTable\").find(\"input[type='checkbox']:checked\").length > 1){\n           syncDataOnlyDiv.removeClass(hidden_class);\n        }else{\n            syncDataOnlyDiv.addClass(hidden_class);\n        }\n        handle_date_selection();\n        lock_interface(false);\n    });\n}\n\nfunction check_date_range(){\n    const start_date = new Date($(\"#startDate\").val());\n    const end_date = new Date($(\"#endDate\").val());\n    return (!isNaN(start_date) && !isNaN(end_date)) ? start_date < end_date : true;\n}\n\nfunction check_date_range_available() {\n    const data_file_checked = $(\".selectable_datafile\").has(\"input[type='checkbox']:checked\");\n    return data_file_checked.length === data_file_checked.has(\"td[data-start-timestamp]\").length;\n}\n\nfunction handle_date_selection(){\n    if(!check_date_range_available()){\n        startDate.prop(\"disabled\", true);\n        endDate.prop(\"disabled\", true);\n        return;\n    }\n    startDate.prop(\"disabled\", false);\n    endDate.prop(\"disabled\", false);\n    const data_file_checked_with_date_range = $(\".selectable_datafile\").has(\"input[type='checkbox']:checked\")\n                                                .has(\"td[data-end-timestamp]\");\n    if(data_file_checked_with_date_range.length === 0){\n        return;\n    }\n    let end_timestamps = [];\n    let start_timestamps = [];\n    data_file_checked_with_date_range.find(\"[data-end-timestamp\").each(function(){\n        end_timestamps.push(parseInt($(this).attr(\"data-end-timestamp\")));\n        start_timestamps.push(parseInt($(this).attr(\"data-start-timestamp\")));\n    });\n    const start_timestamp = syncDataOnlyCheckbox.prop(\"checked\") ?\n                                Math.max(...start_timestamps) : Math.min(...start_timestamps);\n    const end_timestamp = syncDataOnlyCheckbox.prop(\"checked\") ?\n                                Math.min(...end_timestamps) : Math.max(...end_timestamps);\n\n    const newStartDateTime = new Date(start_timestamp * 1000);\n    const newEndDateTime = new Date(end_timestamp * 1000);\n    const newStartDate = newStartDateTime.toISOString().split(\"T\")[0];\n    const newEndDate = newEndDateTime.toISOString().split(\"T\")[0];\n    if((new Date(startDate[0].value)) < newStartDateTime){\n        startDate.val(newStartDate);\n    }\n    if((new Date(endDate[0].value)) > newEndDateTime){\n        endDate.val(newEndDate);\n    }\n    startDate[0].min = newStartDate;\n    startDate[0].max = newEndDate;\n    endDate[0].max = newEndDate;\n    endDate[0].min = newStartDate;\n}\n\nconst dataFilesTable = $('#dataFilesTable').DataTable({\n    \"order\": [[ 2, 'desc' ]],\n    \"columnDefs\": [\n      { \"width\": \"20%\", \"targets\": 2 },\n      { \"width\": \"8%\", \"targets\": 5 },\n    ],\n    \"destroy\": true\n});\nconst syncDataOnlyDiv = $(\"#synchronized-data-only-div\");\nconst syncDataOnlyCheckbox = $(\"#synchronized-data-only-checkbox\");\nconst startDate = $(\"#startDate\");\nconst endDate = $(\"#endDate\");\n\n$(document).ready(function() {\n    lock_interface_callbacks.push(function () {\n        return get_selected_files() <= 0;\n    });\n    handle_backtesting_buttons();\n    handle_file_selection();\n    $('#dataFilesTable').on(\"draw.dt\", function(){\n        handle_file_selection();\n    });\n    lock_interface();\n\n    init_backtesting_status_websocket();\n\n    syncDataOnlyCheckbox.on(\"change\", handle_date_selection);\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/commands.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction load_commands_metadata() {\n    const feedbackButton = $(\"#feedbackButton\");\n    if(feedbackButton.length > 0){\n        $.get({\n            url: feedbackButton.attr(update_url_attr),\n            dataType: \"json\",\n            success: function(msg, status){\n                if(msg) {\n                    feedbackButton.attr(\"href\", msg);\n                    feedbackButton.removeClass(\"disabled\");\n                }else{\n                    setNoFeedback(feedbackButton);\n                }\n            },\n            error: function(result, status, error){\n                setNoFeedback(feedbackButton);\n                window.console&&console.error(\"Impossible to get the current OctoBot feedback form: \"+error);\n            }\n        })\n    }\n}\n\nfunction setNoFeedback(feedbackButton){\n    feedbackButton.text(\"No feedback system available for now\");\n}\n\nfunction update_metrics_option(){\n    const metrics_input = $(\"#metricsCheckbox\");\n    function metrics_success_callback(updated_data, update_url, dom_root_element, msg, status) {\n        if(updated_data){\n            create_alert(\"success\", \"Anonymous statistics enabled\", \"Thank you for supporting OctoBot development!\");\n        }else{\n            create_alert(\"success\", \"Anonymous statistics disabled\", \"\");\n        }\n    }\n    send_and_interpret_bot_update(metrics_input.is(':checked'), metrics_input.attr(update_url_attr), null,\n        metrics_success_callback, update_failure_callback);\n}\n\nfunction update_beta_option(){\n    function beta_success_callback(updated_data, update_url, dom_root_element, msg, status) {\n        const details = \"Please restart your OctoBot for it to take effect.\"\n        if(updated_data){\n            create_alert(\"success\", \"Beta environment enabled\", details);\n        }else{\n            create_alert(\"success\", \"Beta environment disabled\", details);\n        }\n    }\n    const beta_input = $(\"#beta-checkbox\");\n    send_and_interpret_bot_update(beta_input.is(':checked'), beta_input.attr(update_url_attr), null,\n        beta_success_callback, update_failure_callback);\n}\n\nfunction update_failure_callback(updated_data, update_url, dom_root_element, msg, status) {\n    create_alert(\"error\", msg.responseText, \"\");\n}\n\n$(document).ready(function() {\n    load_commands_metadata();\n    $(\"#metricsCheckbox\").change(function(){\n        update_metrics_option();\n    });\n    $(\"#beta-checkbox\").change(function(){\n        update_beta_option();\n    });\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/community.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction disablePackagesOperations(should_lock=true){\n    const disabled_attr = 'disabled';\n    $(\"[data-role=\\\"install-strategy\\\"]\").prop(disabled_attr, should_lock);\n}\n\nfunction reloadTable(){\n    $('.table').each(function () {\n        $(this).DataTable({\n            paging: false\n        });\n    });\n    registerPackagesEvents();\n}\n\nfunction registerPackagesEvents(){\n    $(\"[data-role=\\\"install-strategy\\\"]\").click(function (){\n        const element = $(this);\n        const update_url = element.attr(update_url_attr);\n        const data = {\n            \"strategy_id\": element.data(\"strategy-id\"),\n            \"name\": element.data(\"strategy-name\"),\n            \"description\": element.data(\"description\"),\n        };\n        disablePackagesOperations();\n        send_and_interpret_bot_update(data, update_url, element, packagesOperationSuccessCallback, packagesOperationErrorCallback);\n    });\n}\n\nfunction selectProfile(profileId) {\n    if(profileId.length){\n        const changeProfileURL = $(\"#cloud-strategies-selector\").data(\"select-profile-url\").replace(\"PROFILE_ID\", profileId);\n        window.location.replace(changeProfileURL);\n    }\n}\n\nfunction packagesOperationSuccessCallback(updated_data, update_url, dom_root_element, msg, status){\n    disablePackagesOperations(false);\n    const postInstallActions = dom_root_element.data(\"post-install-action\")\n    if(postInstallActions === \"select-profile\"){\n        selectProfile(msg.profile_id)\n    }else{\n        create_alert(\"success\", \"Strategy operation\", msg.text);\n    }\n}\n\nfunction packagesOperationErrorCallback(updated_data, update_url, dom_root_element, result, status, error){\n    disablePackagesOperations(false);\n    create_alert(\"error\", \"Error during strategy operation: \"+result.responseText, \"\");\n}\n\nfunction displayBotSelectorWhenNoSelectedBot(){\n    if($(\"#bot-selector\").find(\"button[data-role='selected-bot']\").length === 0) {\n        // no selected bot, force selection\n        $('#bot-select-modal').modal({backdrop: 'static', keyboard: false})\n    }\n}\n\nfunction disableBotsSelectAndCreate(disabled){\n    $(\"#bot-selector\").find(\"button[data-role='select-bot']\").attr(\"disabled\", disabled);\n    $(\"#create-new-bot\").attr(\"disabled\", disabled);\n}\n\nfunction initBotsCallbacks(){\n    $(\"#bot-selector\").find(\"button[data-role='select-bot']\").click((element) => {\n        const selectButton = $(element.target);\n        const data = selectButton.data(\"bot-id\")\n        disableBotsSelectAndCreate(true);\n        selectButton.html(\"<i class='fa fa-spinner fa-spin'></i>\")\n        const update_url = $(\"#bot-selector\").data(\"update-url\");\n        send_and_interpret_bot_update(data, update_url, null,\n            botOperationSuccessCallback, botOperationErrorCallback);\n\n    })\n    $(\"#create-new-bot\").click((element) => {\n        const createButton = $(element.target);\n        const update_url = createButton.data(\"update-url\");\n        disableBotsSelectAndCreate(true);\n        createButton.html(\"<i class='fa fa-spinner fa-spin'></i> Creating ...\")\n        send_and_interpret_bot_update({}, update_url, null,\n            botOperationSuccessCallback, botOperationErrorCallback);\n    })\n}\n\nfunction botOperationSuccessCallback(updated_data, update_url, dom_root_element, result, status, error){\n    // reload the page to retest bots\n    window.location.reload();\n}\n\nfunction botOperationErrorCallback(updated_data, update_url, dom_root_element, result, status, error){\n    create_alert(\"error\", \"Error when managing bots: \"+result.responseText, \"\");\n}\n\nfunction initLoginSubmit(){\n    $(\"form[name=community-login]\").on(\"submit\", () => {\n        $(\"input[type=submit]\").addClass(hidden_class).attr(\"disabled\", true);\n        $(\"#login-waiter\").removeClass(hidden_class);\n    });\n}\n\n$(document).ready(function() {\n    reloadTable();\n    displayBotSelectorWhenNoSelectedBot();\n    initBotsCallbacks();\n    initLoginSubmit();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/community_metrics.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\n$(document).ready(function() {\n    $('.table').each(function () {\n        $(this).DataTable();\n    });\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/config_tentacle.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction apply_evaluator_default_config(element) {\n    const default_config = element.attr(\"default-elements\").replace(new RegExp(\"'\",\"g\"),'\"');\n    const update_url = $(\"#defaultConfigDiv\").attr(update_url_attr);\n    const updated_config = {};\n    const config_type = element.attr(config_type_attr);\n    updated_config[config_type] = {};\n\n    $.each($.parseJSON(default_config), function (i, config_key) {\n        updated_config[config_type][config_key] = \"true\";\n    });\n\n    updated_config[\"deactivate_others\"] = true;\n\n    // send update\n    send_and_interpret_bot_update(updated_config, update_url, null, handle_apply_evaluator_default_config_success_callback);\n}\n\nfunction handle_apply_evaluator_default_config_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", \"Evaluators activated\", \"Restart OctoBot for changes to be applied\");\n    location.reload();\n}\n\nfunction updateTentacleConfig(updatedConfig, update_url){\n    send_and_interpret_bot_update(updatedConfig, update_url, null, handle_tentacle_config_update_success_callback, handle_tentacle_config_update_error_callback);\n}\n\nfunction factory_reset(update_url){\n    send_and_interpret_bot_update(null, update_url, null, handle_tentacle_config_reset_success_callback, handle_tentacle_config_update_error_callback);\n}\n\nfunction handle_tentacle_config_reset_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", \"Configuration reset\", msg);\n    initConfigEditor(false);\n}\n\nfunction handle_tentacle_config_update_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", \"Configuration saved\", msg);\n    initConfigEditor(false);\n}\n\nfunction handle_tentacle_config_update_error_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"error\", \"Error when updating config\", msg.responseText);\n}\n\nfunction handleConfigDisplay(success){\n    $(\"#editor-waiter\").hide();\n    if(success){\n        $(\"#configErrorDetails\").hide();\n        if(canEditConfig()) {\n            $(\"#saveConfigFooter\").show();\n            $(\"button[data-role='saveConfig']\").removeClass(hidden_class).unbind(\"click\").click(function (event) {\n                const errorsDesc = validateJSONEditor(configEditor);\n                if (errorsDesc.length) {\n                    create_alert(\"error\", \"Error when saving configuration\",\n                        `Invalid configuration data: ${errorsDesc}.`);\n                } else{\n                    const url = $(event.currentTarget).attr(update_url_attr)\n                    updateTentacleConfig(configEditor.getValue(), url);\n                }\n            });\n        }else{\n            $(\"#noConfigMessage\").show();\n        }\n    }else{\n        $(\"#configErrorDetails\").show();\n    }\n}\n\nfunction get_config_value_changed(element, new_value) {\n    let new_value_str = new_value.toString().toLowerCase();\n    return new_value_str !== element.attr(config_value_attr).toLowerCase();\n}\n\nfunction handle_save_buttons_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    update_dom(dom_root_element, msg);\n    create_alert(\"success\", \"Configuration successfully updated\", \"Restart OctoBot for changes to be applied.\");\n}\n\nfunction send_command_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", `${updated_data.subject} command sent`, \"\");\n}\n\nfunction handle_save_button(){\n    $(\"#saveActivationConfig\").click(function() {\n        const full_config = $(\"#activatedElementsBody\");\n        const updated_config = {};\n        const update_url = $(\"#saveActivationConfig\").attr(update_url_attr);\n\n        full_config.find(\".\"+config_element_class).each(function(){\n            const config_type = $(this).attr(config_type_attr);\n\n            if(!(config_type in updated_config)){\n                updated_config[config_type] = {};\n            }\n\n            const new_value = parse_new_value($(this));\n            const config_key = get_config_key($(this));\n\n            if(get_config_value_changed($(this), new_value)){\n                updated_config[config_type][config_key] = new_value;\n            }\n        });\n\n        // send update\n        send_and_interpret_bot_update(updated_config, update_url, full_config, handle_save_buttons_success_callback);\n    })\n}\n\nfunction handleUserCommands(){\n    $(\".user-command\").click(function () {\n        const button = $(this);\n        const update_url = button.attr(\"update-url\");\n        const commandData = {};\n        button.parents(\".modal-content\").find(\".command-param\").each(function (){\n            const element = $(this);\n            commandData[element.data(\"param-name\")] = element.val();\n        })\n        const data = {\n            action: button.data(\"action\"),\n            subject: button.data(\"subject\"),\n            data: commandData,\n        };\n        send_and_interpret_bot_update(data, update_url, null, send_command_success_callback);\n    });\n}\n\nfunction handleButtons() {\n    handle_save_button();\n    handleUserCommands();\n\n    $(\"#applyDefaultConfig\").click(function () {\n        const tentacle_name = $(this).attr(\"tentacle\");\n        apply_evaluator_default_config($(\"#\" + tentacle_name));\n    });\n\n    $(\"#startBacktesting\").click(function(){\n        if(!check_date_range()){\n            create_alert(\"error\", \"Invalid date range.\", \"\");\n            return;\n        }\n        $(\"#backtesting_progress_bar\").show();\n        lock_interface();\n        const request = {};\n        request[\"files\"] = get_selected_files();\n        const startDate = $(\"#startDate\");\n        const endDate = $(\"#endDate\");\n        request[\"start_timestamp\"] = startDate.val().length ? (new Date(startDate.val()).getTime()) : null;\n        request[\"end_timestamp\"] = endDate.val().length ? (new Date(endDate.val()).getTime()) : null;\n        const update_url = $(\"#startBacktesting\").attr(\"start-url\");\n        start_backtesting(request, update_url);\n    });\n\n    $(\"button[data-role='factoryResetConfig']\").click(function(){\n        if (confirm(\"Reset this tentacle configuration to its default values ?\") === true) {\n            factory_reset($(\"button[data-role='factoryResetConfig']\").attr(\"update-url\"));\n        }\n    });\n    \n    $(\"#reloadBacktestingPart\").click(function () {\n        window.location.hash = \"backtestingInputPart\";\n        location.reload();\n    })\n}\nfunction check_date_range(){\n    const start_date = new Date($(\"#startDate\").val());\n    const end_date = new Date($(\"#endDate\").val());\n    return (!isNaN(start_date) && !isNaN(end_date)) ? start_date < end_date : true;\n}\n\nfunction get_config_key(elem){\n    return elem.attr(config_key_attr);\n}\n\nfunction parse_new_value(element) {\n    return element.attr(current_value_attr).toLowerCase();\n}\n\nfunction handle_evaluator_configuration_editor(){\n    $(\".config-element\").click(function(e){\n        if (isDefined($(e.target).attr(no_activation_click_attr))){\n            // do not trigger when click on items with no_activation_click_attr set\n            return;\n        }\n        const element = $(this);\n\n        if (element.hasClass(config_element_class)){\n\n            if (element[0].hasAttribute(config_type_attr) && (element.attr(config_type_attr) === evaluator_config_type || element.attr(config_type_attr) === trading_config_type)){\n\n                // build data update\n                let new_value;\n                let current_value = parse_new_value(element);\n\n                if (current_value === \"true\"){\n                    new_value = \"false\";\n                }else if(current_value === \"false\"){\n                    new_value = \"true\";\n                }\n\n                // update current value\n                element.attr(current_value_attr, new_value);\n\n                //update dom\n                update_element_temporary_look(element);\n            }\n        }\n    });\n}\n\nfunction something_is_unsaved(){\n    let edited_config = canEditConfig() ? getValueChangedFromRef(\n        configEditor.getValue(), savedConfig, true\n    ) : false;\n    return (\n        edited_config\n        || $(\"#super-container\").find(\".\"+modified_badge).length > 0\n    )\n}\n\nfunction get_selected_files(){\n    return [$(\"#dataFileSelect\").val()];\n}\n\n\nfunction canEditConfig() {\n    return parsedConfigSchema && parsedConfigValue\n}\n\nlet configEditor = null;\nlet configEditorChangeEventCallbacks = [];\nlet savedConfig = null;\nlet parsedConfigSchema = null;\nlet parsedConfigValue = null;\nlet startingConfigValue = null;\n\nfunction _addGridDisplayOptions(schema){\n    if(typeof schema.properties === \"undefined\" && typeof schema.items === \"undefined\"){\n        return;\n    }\n    // display user inputs as grid\n    // if(typeof schema.format === \"undefined\") {\n    //     schema.format = \"grid\";\n    // }\n    if(typeof schema.options === \"undefined\"){\n        schema.options = {};\n    }\n    schema.options.grid_columns = 6;\n    if(typeof schema.properties !== \"undefined\"){\n        Object.values(schema.properties).forEach (property => {\n            _addGridDisplayOptions(property)\n        });\n    }\n    if(typeof schema.items !== \"undefined\"){\n        _addGridDisplayOptions(schema.items)\n    }\n}\n\nfunction initConfigEditor(showWaiter) {\n    if(showWaiter){\n        $(\"#editor-waiter\").show();\n    }\n    const configEditorBody = $(\"#configEditorBody\");\n\n    function editDetailsSuccess(updated_data, update_url, dom_root_element, msg, status){\n        const inputs = msg[\"displayed_elements\"][\"data\"][\"elements\"];\n        if(inputs.length === 0){\n            handleConfigDisplay(true);\n            return;\n        }\n        parsedConfigValue = msg[\"config\"];\n        savedConfig = parsedConfigValue\n        parsedConfigSchema = inputs[0][\"schema\"];\n        parsedConfigSchema.id = \"tentacleConfig\"\n        if(configEditor !== null){\n            configEditor.destroy();\n        }\n        if (canEditConfig()){\n            fix_config_values(parsedConfigValue, parsedConfigSchema)\n        }\n        _addGridDisplayOptions(parsedConfigSchema);\n        const settingsRoot = $(\"#configEditor\");\n        configEditor = canEditConfig() ? (new JSONEditor(settingsRoot[0],{\n            schema: parsedConfigSchema,\n            startval: parsedConfigValue,\n            no_additional_properties: true,\n            prompt_before_delete: true,\n            disable_array_reorder: true,\n            disable_collapse: true,\n            disable_properties: true,\n            disable_edit_json: true,\n        })) : null;\n        settingsRoot.find(\"select[multiple=\\\"multiple\\\"]\").select2({\n            width: 'resolve', // need to override the changed default\n            closeOnSelect: false,\n            placeholder: \"Select values to use\"\n        });\n        const configEditorButtons = $(\"#configEditorButtons\");\n        if(configEditor !== null){\n            configEditor.on(\"change\", editorChangeCallback);\n            if(configEditorButtons.length){\n                configEditorButtons.removeClass(hidden_class);\n            }\n        } else {\n            if(configEditorButtons.length){\n                configEditorButtons.addClass(hidden_class);\n            }\n        }\n        handleConfigDisplay(true);\n    }\n\n    const editDetailsFailure = (updated_data, update_url, dom_root_element, msg, status) => {\n        create_alert(\"error\", \"Error when fetching tentacle config\", msg.responseText);\n        handleConfigDisplay(false);\n    }\n\n\n    send_and_interpret_bot_update(null, configEditorBody.data(\"edit-details-url\"), null,\n        editDetailsSuccess, editDetailsFailure, \"GET\");\n}\n\nfunction editorChangeCallback(){\n    if(validateJSONEditor(configEditor) === \"\" && something_is_unsaved()){\n        configEditorChangeEventCallbacks.forEach((callback) => {\n            callback(configEditor.getValue());\n        });\n    }\n}\n\nfunction addEditorChangeEventCallback(callback){\n    configEditorChangeEventCallbacks.push(callback)\n}\n\n$(document).ready(function() {\n    initConfigEditor(true);\n    handleButtons();\n    if(typeof lock_interface !== \"undefined\"){\n        lock_interface(false);\n    }\n\n    handle_evaluator_configuration_editor();\n\n    if(typeof init_backtesting_status_websocket !== \"undefined\"){\n        init_backtesting_status_websocket();\n    }\n\n    register_exit_confirm_function(something_is_unsaved);\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/configuration.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\nconst sidebarNavLinks = $(\".sidebar\").find(\".nav-link[role='tab']:not(.dropdown-toggle)\");\n\nfunction handle_nested_sidenav(){\n    sidebarNavLinks.each(function (){\n        $(this).on(\"click\",function (e){\n            e.preventDefault();\n            activate_tab($(this), sidebarNavLinks);\n        });\n    });\n}\n\nfunction get_tabs_config(){\n    return $(document).find(\".\" + config_root_class + \" .\" + config_container_class);\n}\n\n\nfunction handle_reset_buttons(){\n    $(\"#reset-config\").click(function() {\n        reset_configuration_element();\n    })\n}\n\nfunction handle_remove_buttons(){\n    // Card deck removing\n    $(document).on(\"click\", \".remove-btn\", function() {\n        const deleted_element_key = get_card_config_key($(this));\n        const deck = get_deck_container($(this));\n        const card = get_card_container($(this));\n        if ($.inArray(deleted_element_key, deleted_global_config_elements) === -1 && !card.hasClass(added_class)){\n            deleted_global_config_elements.push(deleted_element_key);\n        }\n        $(this).closest(\".card\").fadeOut(\"normal\", function() {\n            $(this).remove();\n            check_deck_modifications(deck);\n        });\n    });\n}\n\nfunction handle_buttons() {\n    $(\"button[action=post]\").each(function () {\n        $(this).click(function () {\n            send_and_interpret_bot_update(null, $(this).attr(update_url_attr), null, generic_request_success_callback, generic_request_failure_callback);\n        });\n    });\n}\n\nfunction check_deck_modifications(deck){\n    if(deck.find(\".\"+added_class).length > 0 || deleted_global_config_elements.length > 0){\n        toogle_deck_container_modified(deck);\n    }else{\n        toogle_deck_container_modified(deck, false);\n    }\n}\n\nfunction handle_add_buttons(){\n    handleCardDecksAddButtons();\n    handleEditableAddButtons();\n}\n\nfunction handleEditableAddButtons(){\n    $(\"button[data-role='editable-add']\").click((jsElement) => {\n        const button = $(jsElement.currentTarget);\n        const parentContainer = button.parent();\n        const targetTemplate = parentContainer.find(`span[data-add-template-for='${button.attr(\"data-add-template-target\")}']`);\n        const selectedValue = button.data(\"default-key\");\n        let newEditable = targetTemplate.html().replace(new RegExp(\"Empty\",\"g\"), selectedValue);\n        button.before(newEditable);\n        handle_editable();\n        register_edit_events();\n    })\n}\n\nfunction handleEditableRenameIfNotAlready(e, params){\n    const element = $(e.target);\n    // 0. update key-value config to use the new key\n    const previousKey = element.text().trim();\n    let newKey = element.text().trim();\n    if(isDefined(params) && isDefined(params[\"newValue\"])){\n        newKey = params[\"newValue\"];\n    }\n    const previousConfigKey = element.attr(\"data-label-for\");\n    const valueToUpdate = element.parent().parent().find(`a[config-key=${previousConfigKey}]`);\n    const newConfigKey = previousConfigKey.replace(new RegExp(previousKey,\"g\"), newKey);\n    element.attr(\"data-label-for\", newConfigKey)\n    valueToUpdate.attr(\"config-key\", newConfigKey)\n    // 1. force change to the associated value to save it\n    valueToUpdate.data(\"changed\", true);\n    // 2. add previous key to deleted values unless it's the default key\n    deleted_global_config_elements.push(previousConfigKey);\n    const card_container = get_card_container(element);\n    toogle_card_modified(card_container, true);\n}\n\nfunction registerHandleEditableRenameIfNotAlready(element, events, handler){\n    if(typeof element.data(\"label-for\") !== \"undefined\"){\n        events.forEach((event) => {\n            if(!check_has_event_using_handler(element, event, handler)){\n                element.on(event, handler);\n            }\n        })\n    }\n}\n\nfunction handleCardDecksAddButtons(){\n    // Card deck adding\n    $(\".add-btn\").click(function() {\n\n        const button_id = $(this).attr(\"id\");\n\n        const deck = $(this).parents(\".\" + config_root_class).find(\".card-deck\");\n        const select_input = $(\"#\" + button_id + \"Select\");\n        let select_value = select_input.val();\n\n        // currencies\n        const currencyDetails = currencyDetailsById[select_value];\n        let select_symbol = \"\";\n        let currency_id = undefined;\n        if(isDefined(currencyDetails)){\n            currency_id = select_value;\n            select_value = currencyDetails.n;\n            select_symbol = currencyDetails.s\n        }\n\n        // exchanges\n        let has_websockets = false;\n        const ws_attr = select_input.find(\"[data-tokens='\"+select_value+\"']\").attr(\"data-ws\");\n        if(isDefined(ws_attr)){\n            has_websockets = ws_attr === \"True\";\n        }\n\n        const editable_selector = \"select[editable_config_id=\\\"multi-select-element-\" + select_value + \"\\\"]:first\";\n        let target_template = $(\"#\" + button_id + \"-template-default\");\n\n        //services\n        const in_services = button_id === \"AddService\";\n        if (in_services){\n            target_template = $(\"#\" + button_id + \"-template-default-\"+select_value);\n        }\n\n        // check if not already added\n        if(deck.find(\"div[name='\"+select_value+\"']\").length === 0){\n            let template_default = target_template.html().replace(new RegExp(config_default_value,\"g\"), select_value);\n            template_default = template_default.replace(new RegExp(\"card-text symbols default\",\"g\"), \"card-text symbols\");\n            template_default = template_default.replace(new RegExp(\"card-img-top currency-image default\",\"g\"), \"card-img-top currency-image\");\n            if(isDefined(currency_id)){\n                template_default = template_default.replace(new RegExp(`data-currency-id=\"${config_default_value.toLowerCase()}\"`), `data-currency-id=\"${currency_id}\"`);\n            }\n            if(has_websockets){\n                // all exchanges cards\n                template_default = template_default.replace(new RegExp(\"data-role=\\\"websocket-mark\\\" class=\\\"d-none \"), \"data-role=\\\"websocket-mark\\\" class=\\\"\");\n            }\n            deck.append(template_default).hide().fadeIn();\n\n            handle_editable();\n\n            // select options with reference market if any\n            $(editable_selector).each(function () {\n                if (\n                    $(this).siblings('.select2').length === 0\n                    && !$(this).parent().hasClass('default')\n                ){\n                    $(this).find(\"option\").each(function () {\n                        const option = $(this);\n                        const symbols = option.attr(\"value\").split(\"/\");\n                        const reference_market = select_input.attr(\"reference_market\").toUpperCase();\n                        if (symbols[0] === select_symbol && symbols[1] === reference_market){\n                            option.attr(\"selected\", \"selected\");\n                        }\n                        // remove options without this currency symbol\n                        if (!(symbols[0] === select_symbol || symbols[1] === select_symbol)){\n                            option.detach();\n                        }\n                    });\n                }\n            });\n\n            let placeholder = \"\";\n            if(select_symbol){\n                placeholder = \"Select trading pair(s)\";\n            }else if(in_services){\n                // telegram is the only service with a select2 element\n                placeholder = \"Add user(s) in whitelist\";\n            }\n\n            // add select2 selector\n            $(editable_selector).each(function () {\n                if (\n                    $(this).siblings('.select2').length === 0\n                    && !$(this).parent().hasClass('default')\n                ) {\n                    $(this).select2({\n                        width: 'resolve', // need to override the changed default\n                        tags: true,\n                        placeholder: placeholder,\n                    });\n                }\n            });\n\n            toogle_deck_container_modified(get_deck_container($(this)));\n            // refresh images if required\n            fetch_images();\n            handleDefaultImages();\n\n            register_edit_events();\n        }\n\n    });\n}\n\nfunction handle_special_values(currentElem){\n    if (currentElem.is(traderSimulatorCheckbox) || currentElem.is(traderCheckbox)){\n        if (currentElem.is(\":checked\")){\n            const otherElem = currentElem.is(traderCheckbox) ? traderSimulatorCheckbox : traderCheckbox;\n            otherElem.prop('checked', false);\n            otherElem.trigger(\"change\");\n        }\n    } else if(currentElem.is(tradingReferenceMarket)) {\n        display_generic_modal(\"Change reference market\",\n            \"Do you want to adapt the reference market for all your configured pairs ?\",\n            \"\",\n            function () {\n                let url = \"/api/change_reference_market_on_config_currencies\";\n                let data = {};\n                data[\"old_base_currency\"] = tradingReferenceMarket.attr(config_value_attr);\n                data[\"new_base_currency\"] = tradingReferenceMarket.text();\n                send_and_interpret_bot_update(data, url, null, generic_request_success_callback, generic_request_failure_callback);\n            },\n            null);\n    } else if(currentElem.data(\"summary-field\") === \"radio-select\"){\n        currentElem.find('input[type=\"radio\"]').each((index, element) => {\n            const parsedElement = $(element);\n            if(parsedElement.is(\":checked\")){\n                currentElem.attr(\"current-value\", parsedElement.attr(\"value\"));\n            }\n        })\n    }\n}\n\nfunction register_edit_events(){\n    $('.config-element').each(function (){\n        const element = $(this);\n        if(typeof element.data(\"label-for\") === \"undefined\"){\n            add_event_if_not_already_added(element, 'save', card_edit_handler);\n            add_event_if_not_already_added(element, 'change', card_edit_handler);\n        }else{\n            registerHandleEditableRenameIfNotAlready(element, ['save', 'change'], handleEditableRenameIfNotAlready)\n        }\n    });\n    register_exchanges_checks(false);\n}\n\nfunction card_edit_handler(e, params){\n    const current_elem = $(this);\n\n    handle_special_values(current_elem);\n\n    let new_value = parse_new_value(current_elem);\n    if(isDefined(params) && isDefined(params[\"newValue\"])){\n        new_value = params[\"newValue\"];\n    }\n    const config_key = get_config_key(current_elem);\n    const card_container = get_card_container(current_elem);\n\n    const other_config_elements = card_container.find(\".\"+config_element_class);\n    let something_changed = get_config_value_changed(current_elem, new_value, config_key);\n\n    if(!something_changed){\n        // if nothing changed on the current field, check other fields of the card\n        $.each(other_config_elements, function () {\n            if ($(this)[0] !== current_elem[0]){\n                var elem_new_value = parse_new_value($(this));\n                var elem_config_key = get_config_key($(this));\n                something_changed = something_changed || get_config_value_changed($(this), elem_new_value, elem_config_key);\n            }\n\n        });\n    }\n\n    toogle_card_modified(card_container, something_changed);\n\n}\n\nfunction something_is_unsaved(){\n\n    const config_root = $(\"#super-container\");\n    return (\n        config_root.find(\".\"+card_class_modified).length > 0\n            || config_root.find(\".\"+deck_container_modified_class).length > 0\n            || config_root.find(\".\"+modified_badge).length > 0\n    )\n}\n\nfunction parse_new_value(element){\n    const raw_data = element.text().trim();\n\n    // simple case\n    if(element[0].hasAttribute(current_value_attr)){\n        const value = element.attr(current_value_attr).trim();\n        if(element[0].hasAttribute(config_data_type_attr)){\n            switch(element.attr(config_data_type_attr)) {\n                case \"bool\":\n                    return value === true || value === \"true\";\n                case \"number\":\n                    return Number(value);\n                default:\n                    return value;\n            }\n        }else{\n            return value;\n        }\n    }\n    // with data type\n    else if(element[0].hasAttribute(config_data_type_attr)){\n        switch(element.attr(config_data_type_attr)) {\n            case \"bool\":\n                return element.is(\":checked\");\n            case \"list\":\n                const new_value = [];\n                element.find(\":selected\").each(function(index, value){\n                    new_value.splice(index, 0, value.text.trim());\n                });\n                return new_value;\n            case \"number\":\n                return Number(raw_data);\n            default:\n                return raw_data;\n        }\n\n    // without information\n    }else{\n        return raw_data;\n    }\n}\n\nfunction _save_config(element, restart_after_save) {\n    const full_config = $(\"#super-container\");\n    const updated_config = {};\n    const update_url = element.attr(update_url_attr);\n\n    // take all tabs into account\n    get_tabs_config().each(function(){\n        $(this).find(\".\"+config_element_class).each(function(){\n            const configElement = $(this)\n            if(configElement.parent().parent().hasClass(hidden_class)\n               || typeof configElement.attr(\"data-label-for\") !== \"undefined\"){\n                // do not add hidden elements (add templates)\n                // do not add element labels\n                return\n            }\n            const config_type = configElement.attr(config_type_attr);\n            if(config_type !== evaluator_list_config_type) {\n\n                if (!(config_type in updated_config)) {\n                    updated_config[config_type] = {};\n                }\n\n                const new_value = parse_new_value(configElement);\n                const config_key = get_config_key(configElement);\n\n                if (get_config_value_changed(configElement, new_value, config_key)\n                    && !config_key.endsWith(\"_Empty\")) {\n                    updated_config[config_type][config_key] = new_value;\n                }\n            }\n        })\n    });\n\n    // take removed elements into account\n    updated_config[\"removed_elements\"] = deleted_global_config_elements;\n\n    updated_config[\"restart_after_save\"] = restart_after_save;\n\n    // send update\n    send_and_interpret_bot_update(updated_config, update_url, full_config, handle_save_buttons_success_callback);\n}\n\nfunction handle_save_buttons(){\n    $(\"#save-config\").click(function() {\n        _save_config($(this), false);\n    })\n    $(\"#save-config-and-restart\").click(function() {\n        _save_config($(this), true);\n    })\n}\n\nfunction get_config_key(elem){\n    return elem.attr(config_key_attr);\n}\n\nfunction get_card_config_key(card_component, config_type=\"global_config\"){\n    const element_with_config = card_component.parent(\".card-body\");\n    return get_config_key(element_with_config);\n}\n\nfunction get_deck_container(elem) {\n    return elem.parents(\".\"+deck_container_class);\n}\n\nfunction get_card_container(elem) {\n    return elem.parents(\".\"+config_card_class);\n}\n\nfunction get_config_value_changed(element, new_value, config_key) {\n    let new_value_str = new_value.toString();\n    if(new_value instanceof Array && new_value.length > 0){\n        //need to format array to match python string representation of config\n        var str_array = [];\n        $.each(new_value, function(i, val) {\n            str_array.push(\"'\"+val+\"'\");\n        });\n        new_value_str = \"[\" + str_array.join(\", \") + \"]\";\n    }\n    return get_value_changed(new_value_str, element.attr(config_value_attr).trim(), config_key)\n        || element.data(\"changed\") === true;\n}\n\nfunction get_value_changed(new_val, dom_conf_val, config_key){\n    const lower_case_val = new_val.toLowerCase();\n    if(is_different_value(new_val, lower_case_val, dom_conf_val)){\n        // only push update if the new value is not the previously updated one\n        if (has_element_already_been_updated(config_key)) {\n            return !has_update_already_been_applied(lower_case_val, config_key);\n        }\n        return true;\n    }else{\n        // nothing changed in DOM but the previously updated value might be different (ex: back on initial value)\n        // only push update if the new value is not the previously updated one\n        if (has_element_already_been_updated(config_key)) {\n            return !has_update_already_been_applied(lower_case_val, config_key);\n        }\n        return false;\n    }\n}\n\nfunction is_different_value(new_val, lower_case_new_val, dom_conf_val){\n    return !(lower_case_new_val === dom_conf_val.toLowerCase() ||\n        ((Number(new_val) === Number(dom_conf_val) && $.isNumeric(new_val))));\n}\n\nfunction has_element_already_been_updated(config_key){\n    return config_key in validated_updated_global_config;\n}\n\nfunction has_update_already_been_applied(lower_case_val, config_key){\n    return lower_case_val === validated_updated_global_config[config_key].toString().toLowerCase();\n}\n\nfunction handle_save_buttons_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    updated_validated_updated_global_config(msg[\"global_updated_config\"]);\n    update_dom(dom_root_element, msg);\n    create_alert(\"success\", \"Configuration successfully updated\", \"Restart OctoBot for changes to be applied.\");\n}\n\nfunction apply_evaluator_default_config(element) {\n    const default_config = element.attr(\"default-elements\").replace(new RegExp(\"'\",\"g\"),'\"');\n    const update_url = $(\"#save-config\").attr(update_url_attr);\n    const updated_config = {};\n    const config_type = element.attr(config_type_attr);\n    updated_config[config_type] = {};\n\n    $.each($.parseJSON(default_config), function (i, config_key) {\n        updated_config[config_type][config_key] = \"true\";\n    });\n\n    updated_config[\"deactivate_others\"] = true;\n\n    // send update\n    send_and_interpret_bot_update(updated_config, update_url, null, handle_apply_evaluator_default_config_success_callback);\n}\n\nfunction handle_apply_evaluator_default_config_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", \"Evaluators activated\", \"Restart OctoBot for changes to be applied\");\n}\n\nfunction other_element_activated(root_element){\n    let other_activated_modes_count = root_element.children(\".\"+success_list_item).length;\n    return other_activated_modes_count > 1;\n}\n\nfunction deactivate_other_elements(element, root_element) {\n    const element_id = element.attr(\"id\");\n    root_element.children(\".\"+success_list_item).each(function () {\n        const element = $(this);\n        if(element.attr(\"id\") !== element_id){\n            element.attr(current_value_attr, \"false\");\n            update_element_temporary_look(element);\n        }\n    })\n}\n\nfunction updateTradingModeSummary(selectedElement){\n    const elementDocModal = $(`#${selectedElement.attr(\"name\")}Modal`);\n    const elementDoc = elementDocModal.find(\".modal-body\").text().trim();\n    const blocks = elementDoc.trim().split(\".\\n\");\n    let summaryBlocks = `${blocks[0]}.`;\n    if (summaryBlocks.length < 80 && blocks.length > 1){\n        summaryBlocks = `${summaryBlocks} ${blocks[1]}.`;\n    }\n    $(\"#selected-trading-mode-summary\").html(summaryBlocks);\n}\n\nfunction updateStrategySelector(required_elements){\n    const noStrategyInfo = $(\"#no-strategy-info\");\n    const strategyConfig = $(\"#evaluator-config-root\");\n    const strategyConfigFooter = $(\"#evaluator-config-root-footer\");\n    if (required_elements.length > 1) {\n        noStrategyInfo.addClass(hidden_class);\n        strategyConfig.removeClass(hidden_class);\n        strategyConfigFooter.removeClass(hidden_class);\n    } else {\n        noStrategyInfo.removeClass(hidden_class);\n        strategyConfig.addClass(hidden_class);\n        strategyConfigFooter.addClass(hidden_class);\n    }\n}\n\nfunction update_requirement_activation(element) {\n    const required_elements = element.attr(\"requirements\").split(\"'\");\n    const default_elements = element.attr(\"default-elements\").split(\"'\");\n    $(\"#evaluator-config-root\").children(\".config-element\").each(function () {\n        const element = $(this);\n        if(required_elements.indexOf(element.attr(\"id\")) !== -1){\n            if(default_elements.indexOf(element.attr(\"id\")) !== -1){\n                element.attr(current_value_attr, \"true\");\n            }\n            update_element_temporary_look(element);\n            update_element_required_marker_and_usability(element, true);\n        }else{\n            element.attr(current_value_attr, \"false\");\n            update_element_temporary_look(element);\n            update_element_required_marker_and_usability(element, false);\n        }\n    });\n    updateStrategySelector(required_elements);\n}\n\nfunction get_activated_strategies_count() {\n    return $(\"#evaluator-config-root\").children(\".\"+success_list_item).length\n}\n\nfunction get_activated_trading_mode_min_strategies(){\n    const activated_trading_modes = $(\"#trading-modes-config-root\").children(\".\"+success_list_item);\n    if(activated_trading_modes.length > 0) {\n        return parseInt(activated_trading_modes.attr(\"requirements-min-count\"));\n    }else{\n        return 1;\n    }\n}\n\nfunction check_evaluator_configuration() {\n    const trading_modes = $(\"#trading-modes-config-root\");\n    if(trading_modes.length) {\n        const activated_trading_modes = trading_modes.children(\".\" + success_list_item);\n        if (activated_trading_modes.length) {\n            const required_elements = activated_trading_modes.attr(\"requirements\").split(\"'\");\n            let at_least_one_activated_element = false;\n            $(\"#evaluator-config-root\").children(\".config-element\").each(function () {\n                const element = $(this);\n                if (required_elements.indexOf(element.attr(\"id\")) !== -1) {\n                    at_least_one_activated_element = true;\n                    update_element_required_marker_and_usability(element, true);\n                } else {\n                    update_element_required_marker_and_usability(element, false);\n                }\n            });\n            if (required_elements.length > 1 && !at_least_one_activated_element) {\n                create_alert(\"error\", \"Trading modes require at least one strategy to work properly, please activate the \" +\n                    \"strategy(ies) you want for the selected mode.\", \"\");\n            }\n           updateStrategySelector(required_elements);\n           updateTradingModeSummary(activated_trading_modes);\n        } else {\n            create_alert(\"error\", \"No trading mode activated, OctoBot need at least one trading mode.\", \"\");\n        }\n    }\n}\n\nfunction handle_activation_configuration_editor(){\n    $(\".config-element\").click(function(e){\n        if (isDefined($(e.target).attr(no_activation_click_attr))){\n            // do not trigger when click on items with no_activation_click_attr set\n            return;\n        }\n        const element = $(this);\n\n        if (element.hasClass(config_element_class) && ! element.hasClass(disabled_class)){\n\n            if (element[0].hasAttribute(config_type_attr)) {\n                if(element.attr(config_type_attr) === evaluator_config_type\n                    || element.attr(config_type_attr) === trading_config_type\n                    || element.attr(config_type_attr) === tentacles_config_type) {\n\n                    const is_strategy = element.attr(config_type_attr) === evaluator_config_type;\n                    const is_trading_mode = element.attr(config_type_attr) === trading_config_type;\n                    const is_tentacle = element.attr(config_type_attr) === tentacles_config_type;\n                    const allow_only_one_activated_element = is_trading_mode || is_tentacle;\n\n                    // build data update\n                    let new_value = parse_new_value(element);\n                    let current_value;\n\n                    try {\n                        current_value = element.attr(current_value_attr).toLowerCase();\n                    } catch (e) {\n                        current_value = element.attr(current_value_attr);\n                    }\n                    let root_element = $(\"#trading-modes-config-root\");\n                    if (is_tentacle){\n                        root_element = element.parent(\".config-container\");\n                    }\n                    if (current_value === \"true\") {\n                        if (allow_only_one_activated_element && !other_element_activated(root_element)) {\n                            create_alert(\"error\", \"Impossible to disable all options.\", \"\");\n                            return;\n                        } else if (is_strategy) {\n                            // strategy\n                            const min_strategies = get_activated_trading_mode_min_strategies();\n                            if (get_activated_strategies_count() <= min_strategies) {\n                                create_alert(\"error\", \"This trading mode requires at least \" + min_strategies + \" activated strategies.\", \"\");\n                                return;\n                            }\n                        }\n                        new_value = \"false\";\n                    } else if (current_value === \"false\") {\n                        new_value = \"true\";\n                        if (allow_only_one_activated_element) {\n                            deactivate_other_elements(element, root_element);\n                        }\n                    }\n                    if (is_trading_mode) {\n                        update_requirement_activation(element);\n                        updateTradingModeSummary(element);\n                    }\n\n                    // update current value\n                    element.attr(current_value_attr, new_value);\n\n                    //update dom\n                    update_element_temporary_look(element);\n                }\n                else if (element.attr(config_type_attr) === evaluator_list_config_type){\n                    const strategy_name = element.attr(\"tentacle\");\n                    apply_evaluator_default_config($(\"a[name='\"+strategy_name+\"']\"));\n                }\n            }\n        }\n    });\n}\n\n\nfunction handle_import_currencies(){\n    $(\"#import-currencies-button\").on(\"click\", function(){\n        $(\"#import-currencies-input\").click();\n    });\n    $(\"#import-currencies-input\").on(\"change\", function () {\n        var GetFile = new FileReader();\n        GetFile.onload = function(){\n            let update_url = $(\"#import-currencies-button\").attr(update_url_attr);\n            let data = {};\n            data[\"action\"] = \"update\";\n            data[\"currencies\"] = JSON.parse(GetFile.result);\n            send_and_interpret_bot_update(data, update_url, null,\n                handle_save_buttons_success_callback, generic_request_failure_callback);\n        };\n        GetFile.readAsText(this.files[0]);\n    });\n}\n\n\nfunction handle_export_currencies_button(){\n    $(\"#export-currencies-button\").on(\"click\", function(){\n        update_url = $(\"#export-currencies-button\").attr(update_url_attr);\n        $.get(update_url, null, function(data, status){\n            download_data(JSON.stringify(data), \"currencies_export.json\");\n        });\n    });\n}\n\n\nfunction reset_configuration_element(){\n    remove_exit_confirm_function();\n    location.reload();\n}\n\nfunction updated_validated_updated_global_config(updated_data){\n    for (const conf_key in updated_data) {\n        validated_updated_global_config[conf_key] = updated_data[conf_key];\n    }\n    const to_del_attr = [];\n    $.each(deleted_global_config_elements, function (i, val) {\n        for (const attribute in validated_updated_global_config) {\n            if(attribute.startsWith(val)){\n                to_del_attr.push(attribute);\n            }\n        }\n    });\n    $.each(to_del_attr, function (i, val) {\n        delete validated_updated_global_config[val];\n    });\n    deleted_global_config_elements = [];\n}\n\nfunction fetch_currencies(){\n    const maxDisplayedOptions = 2000;  // display only the first 2000 options to avoid select performance issues\n    const getCurrencyOption = (addCurrencySelect, details) => {\n        return new Option(`${details.n} - ${details.s}`, details.i, false, false);\n    }\n    if(!$(\"#AddCurrencySelect\").length){\n        return\n    }\n    $.get({\n        url: $(\"#AddCurrencySelect\").data(\"fetch-url\"),\n        dataType: \"json\",\n        success: function (data) {\n            const addCurrencySelect = $(\"#AddCurrencySelect\");\n            const options = [];\n            data.slice(0, maxDisplayedOptions).forEach((element) => {\n                if(!currencyDetailsById.hasOwnProperty(element.i)){\n                    currencyDetailsById[element.i] = element\n                }\n                options.push(getCurrencyOption(addCurrencySelect, element))\n            });\n            addCurrencySelect.append(...options);\n            // add selectpicker class at the last moment to avoid refreshing any existing one (slow)\n            addCurrencySelect.addClass(\"selectpicker\")\n            addCurrencySelect.selectpicker('render');\n            // paginatedSelect2(addCurrencySelect, options, pageSize)\n        },\n        error: function (result, status) {\n            window.console && console.error(`Impossible to get currency list: ${result.responseText} (${status})`);\n        }\n    });\n}\n\nlet validated_updated_global_config = {};\nlet deleted_global_config_elements = [];\nlet currencyDetailsById = {}\n\nconst traderSimulatorCheckbox = $(\"#trader-simulator_enabled\");\nconst traderCheckbox = $(\"#trader_enabled\");\nconst tradingReferenceMarket = $(\"#trading_reference-market\");\n\n$(document).ready(function() {\n    handle_nested_sidenav();\n    selectFirstTab(sidebarNavLinks);\n\n    fetch_currencies();\n\n    setup_editable();\n    handle_editable();\n\n    handle_reset_buttons();\n    handle_save_buttons();\n\n    handle_add_buttons();\n    handle_remove_buttons();\n    \n    handle_buttons();\n\n    handle_activation_configuration_editor();\n\n    handle_import_currencies();\n    handle_export_currencies_button();\n\n    register_edit_events();\n\n    register_exit_confirm_function(something_is_unsaved);\n\n    check_evaluator_configuration();\n\n    register_exchanges_checks(true);\n\n    startTutorialIfNecessary(\"profile\");\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/dashboard.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function () {\n    const handleAnnouncementsHide = () => {\n        $(\"button[data-role=\\\"hide-announcement\\\"]\").click(async (event) => {\n            const source = $(event.currentTarget);\n            const url = source.data(\"url\");\n            await async_send_and_interpret_bot_update(undefined, url);\n        })\n    }\n\n    function _refresh_profitability(socket) {\n        socket.emit('profitability');\n        waiting_profitability_update = false;\n    }\n\n    function handle_profitability(socket) {\n        socket.on(\"profitability\", function (data) {\n            updateProfitabilityDisplay(\n                data[\"bot_real_profitability\"], data[\"bot_real_flat_profitability\"],\n                data[\"bot_simulated_profitability\"], data[\"bot_simulated_flat_profitability\"],\n            );\n            if (!waiting_profitability_update) {\n                // re-schedule profitability refresh\n                waiting_profitability_update = true;\n                setTimeout(function () {\n                    _refresh_profitability(socket);\n                }, profitability_update_interval);\n            }\n        })\n    }\n\n    const updateProfitabilityDisplay = (\n        bot_real_profitability, bot_real_flat_profitability,\n        bot_simulated_profitability, bot_simulated_flat_profitability\n    ) => {\n        if(isDefined(bot_real_profitability)){\n            displayProfitability(bot_real_profitability, bot_real_flat_profitability);\n        }\n        else if(isDefined(bot_simulated_profitability)){\n            displayProfitability(bot_simulated_profitability, bot_simulated_flat_profitability);\n        }\n    }\n\n    const displayProfitability = (profitabilityValue, flatValue) => {\n        const displayedValue = parseFloat(profitabilityValue.toFixed(2));\n        const badge = $(\"#profitability-badge\");\n        const flatValueSpan = $(\"#flat-profitability\");\n        const flatValueText = $(\"#flat-profitability-text\");\n        const displayValue = $(\"#profitability-value\");\n        badge.removeClass(hidden_class);\n        flatValueSpan.removeClass(hidden_class);\n        if(profitabilityValue < 0){\n            displayValue.text(displayedValue);\n            flatValueText.text(flatValue);\n            badge.addClass(\"badge-warning\");\n            badge.removeClass(\"badge-success\");\n        } else {\n            displayValue.text(`+${displayedValue}`);\n            flatValueText.text(`+${flatValue}`);\n            badge.removeClass(\"badge-warning\");\n            badge.addClass(\"badge-success\");\n        }\n    }\n\n    function get_in_backtesting_mode() {\n        return $(\"#first_symbol_graph\").attr(\"backtesting_mode\") === \"True\";\n    }\n\n    function init_dashboard_websocket() {\n        socket = get_websocket(\"/dashboard\");\n    }\n\n    function get_version_upgrade() {\n        const upgradeVersionAlertDiv = $(\"#upgradeVersion\");\n        if(upgradeVersionAlertDiv.length){\n            $.get({\n                url: upgradeVersionAlertDiv.attr(update_url_attr),\n                dataType: \"json\",\n                success: function (msg, status) {\n                    if (msg) {\n                        upgradeVersionAlertDiv.text(msg);\n                        upgradeVersionAlertDiv.parent().parent().removeClass(disabled_item_class);\n                    }\n                }\n            })\n        }\n    }\n\n    const onGraphUpdate = (data) => {\n        if (onGraphUpdateCallback !== undefined){\n            onGraphUpdateCallback();\n        }\n        update_graph(data);\n    }\n\n    function handle_graph_update() {\n        socket.on('candle_graph_update_data', function (data) {\n            onGraphUpdate(data);\n        });\n        socket.on('new_data', function (data) {\n            debounce(\n                () => update_graph(data, false),\n                500\n            );\n        });\n        socket.on('error', function (data) {\n            if (\"missing exchange manager\" === data) {\n                socket.off(\"candle_graph_update_data\");\n                socket.off(\"new_data\");\n                socket.off(\"error\");\n                socket.off(\"profitability\");\n                $('#exchange-specific-data').load(document.URL + ' #exchange-specific-data', function (data) {\n                    init_graphs();\n                });\n            }\n        });\n    }\n\n    function _find_symbol_details(symbol, exchange_id) {\n        let found_update_detail = undefined;\n        update_details.forEach((update_detail) => {\n            if (update_detail.symbol.replace(new RegExp(\"/\",\"g\"), \"|\")\n                === symbol.replace(new RegExp(\"/\",\"g\"), \"|\")\n                && update_detail.exchange_id === exchange_id) {\n                found_update_detail = update_detail;\n            }\n        })\n        return found_update_detail;\n    }\n\n    function update_graph(data, re_update = true) {\n        const candle_data = data.data;\n        let update_detail = undefined;\n        if (isDefined(data.request)) {\n            update_detail = data.request;\n            // ensure candles are from the right timeframe\n            const client_update_detail = _find_symbol_details(candle_data.symbol, candle_data.exchange_id);\n            if(typeof client_update_detail !== \"undefined\"\n                && update_detail.time_frame !== client_update_detail.time_frame){\n                // wrong time frame: don't update and don't ask for more update\n                return\n            }\n        } else {\n            update_detail = _find_symbol_details(candle_data.symbol, candle_data.exchange_id);\n        }\n        if (isDefined(update_detail)) {\n            get_symbol_price_graph(update_detail.elem_id, update_details.exchange_id, \"\",\n                \"\", update_details.time_frame, shouldDisplayOrders(), get_in_backtesting_mode(),\n                false, true, 0, candle_data);\n            if (re_update) {\n                setTimeout(function () {\n                    socket.emit(\"candle_graph_update\", update_detail);\n                }, price_graph_update_interval);\n            }\n        }\n    }\n\n    function init_updater(exchange_id, symbol, time_frame, elem_id) {\n        if (!get_in_backtesting_mode()) {\n            let update_detail = _find_symbol_details(symbol, exchange_id);\n            if(typeof update_detail === \"undefined\"){\n                update_detail = {};\n                update_detail.exchange_id = exchange_id;\n                update_detail.symbol = symbol;\n                update_detail.time_frame = time_frame;\n                update_detail.elem_id = elem_id;\n                update_details.push(update_detail);\n            }else{\n                update_detail.time_frame = time_frame;\n            }\n            setTimeout(function () {\n                    if (isDefined(socket)) {\n                        socket.emit(\"candle_graph_update\", update_detail);\n                    }\n                },\n                3000);\n        }\n    }\n\n    function enable_default_graph(time_frame) {\n        $(\"#first_symbol_graph\").removeClass(hidden_class);\n        Plotly.purge(\"graph-symbol-price\");\n        $(\"#graph-symbol-price\").empty();\n        get_first_symbol_price_graph(\"graph-symbol-price\", get_in_backtesting_mode(), init_updater, time_frame, shouldDisplayOrders());\n    }\n\n    function no_data_for_graph(element_id) {\n        document.getElementById(element_id).parentElement.classList.add(hidden_class);\n        if ($(\".candle-graph\").not(`.${hidden_class}`).length === 0) {\n            // enable default graph if no watched symbol graph can be displayed\n            enable_default_graph();\n        }\n    }\n\n    function init_graphs() {\n        update_details = [];\n        updatePriceGraphs();\n        handle_graph_update(socket);\n        handle_profitability(socket);\n    }\n\n    const shouldDisplayOrders = () => {\n        return $(\"#displayOrderToggle\").is(\":checked\");\n    }\n\n    const updatePriceGraphs = () => {\n        let useDefaultGraph = true;\n        const time_frame = $(\"#timeFrameSelect\").val();\n        $(\".watched-symbol-graph\").each(function () {\n            useDefaultGraph = false;\n            const element = $(this);\n            Plotly.purge(element.attr(\"id\"));\n            element.empty();\n            get_watched_symbol_price_graph(element, init_updater, no_data_for_graph, time_frame, shouldDisplayOrders());\n        });\n        if (useDefaultGraph) {\n            enable_default_graph(time_frame);\n        }\n    }\n\n    const updateDisplayTimeFrame = (timeFrame) => {\n        const url = $(\"#timeFrameSelect\").data(\"update-url\");\n        const request = {\n            time_frame: timeFrame,\n        }\n        send_and_interpret_bot_update(request, url, null, undefined, generic_request_failure_callback);\n    }\n\n    const updateDisplayOrders = (display_orders) => {\n        const url = $(\"#displayOrderToggle\").data(\"update-url\");\n        const request = {\n            display_orders: display_orders,\n        }\n        send_and_interpret_bot_update(request, url, null, undefined, generic_request_failure_callback);\n    }\n\n    const registerConfigUpdates = () => {\n        $(\"#timeFrameSelect\").on(\"change\", () => {\n            updateDisplayTimeFrame($(\"#timeFrameSelect\").val())\n            updatePriceGraphs();\n        })\n        $(\"#displayOrderToggle\").on(\"change\", () => {\n            updateDisplayOrders(shouldDisplayOrders());\n            updatePriceGraphs();\n        })\n    }\n\n    let update_details = [];\n    let waiting_profitability_update = false;\n\n    let socket = undefined;\n\n    get_version_upgrade();\n    init_dashboard_websocket();\n    init_graphs();\n    registerConfigUpdates();\n    handleAnnouncementsHide();\n});\n\n\nlet onGraphUpdateCallback = undefined\nfunction registerGraphUpdateCallback(callback) {\n    onGraphUpdateCallback = callback\n}"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/dashboard_tutorial_starter.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function () {\n\n    const displayFeedbackFormIfNecessary = () => {\n        const feedbackFormData = $(\"#feedback-form-data\");\n        if(feedbackFormData.data(\"display-form\") === \"True\") {\n            displayFeedbackForm(\n                feedbackFormData.data(\"form-to-display\"),\n                feedbackFormData.data(\"user-id\"),\n                feedbackFormData.data(\"on-submit-url\"),\n            );\n        }\n    };\n\n    if(!startTutorialIfNecessary(\"home\", displayFeedbackFormIfNecessary)){\n        displayFeedbackFormIfNecessary()\n    }\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/data_collector.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction handle_data_files_buttons(){\n    $(\".delete_data_file\").unbind('click');\n    $('.delete_data_file').click(function () {\n        const request = $(this).attr(\"data-file\");\n        const update_url = $(\"#dataFilesTable\").attr(update_url_attr);\n        send_and_interpret_bot_update(request, update_url, $(this), delete_success_callback, delete_error_callback)\n    });\n\n}\n\nfunction handle_file_selection(){\n    const input_elem = $('#inputFile');\n    const file_name = input_elem.val().split('\\\\').pop();\n    $('#inputFileLabel').html(file_name);\n    const has_valid_name = file_name.indexOf(\".data\") !== -1;\n    $('#importFileButton').attr('disabled', !has_valid_name);\n}\n\nfunction delete_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", msg, \"\");\n    dataFilesTable.row( dom_root_element.parents('tr') )\n        .remove()\n        .draw();\n}\n\nfunction delete_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n    create_alert(\"error\", result.responseText, \"\");\n}\n\nfunction reload_table(){\n    $(\"#collector_data\").load(location.href.split(\"?\")[0] + \" #collector_data\",function(){\n        dataFilesTable = $('#dataFilesTable').DataTable({\n            \"order\": [],\n            \"columnDefs\": [\n              { \"width\": \"20%\", \"targets\": 1 },\n              { \"width\": \"8%\", \"targets\": 4 },\n            ],\n        });\n        handle_data_files_buttons();\n        dataFilesTable.on(\"draw.dt\", function(){\n            handle_data_files_buttons();\n        });\n    });\n}\n\nfunction start_collector(){\n    lock_collector_ui();\n    const request = {};\n    request[\"exchange\"] = $('#exchangeSelect').val();\n    request[\"symbols\"] = $('#symbolsSelect').val();\n    request[\"time_frames\"] = $('#timeframesSelect').val().length ? $('#timeframesSelect').val() : null;\n    request[\"startTimestamp\"] = is_full_candle_history_exchanges() ? (new Date($(\"#startDate\").val()).getTime()) : null;\n    request[\"endTimestamp\"] = is_full_candle_history_exchanges() ? (new Date($(\"#endDate\").val()).getTime()) : null;\n    const update_url = $(\"#collect_data\").attr(update_url_attr);\n    send_and_interpret_bot_update(request, update_url, $(this), collector_success_callback, collector_error_callback);\n}\n\nfunction stop_collector(){\n    const update_url = $(\"#stop_collect_data\").attr(update_url_attr);\n    send_and_interpret_bot_update({}, update_url, $(this), collector_success_callback, collector_error_callback);\n}\n\nfunction collector_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", msg, \"\");\n    reload_table();\n}\n\nfunction collector_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n    create_alert(\"error\", result.responseText, \"\");\n    lock_collector_ui(false);\n}\n\nfunction display_alert(success, message){\n    if(success === \"True\"){\n        create_alert(\"success\", message, \"\");\n    }else{\n        create_alert(\"error\", message, \"\");\n    }\n}\n\nfunction update_symbol_list(url, exchange){\n    const data = {exchange: exchange};\n    $.get(url, data, function(data, status){\n        const symbolSelect = $(\"#symbolsSelect\");\n        symbolSelect.empty(); // remove old options\n        const symbolSelectBox = symbolSelect[0];\n        $.each(data, function(key,value) {\n            symbolSelectBox.append(new Option(value,value));\n        });\n        symbolSelect.trigger('change');\n    });\n}\n\nfunction update_available_timeframes_list(url, exchange){\n    const data = {exchange: exchange};\n    $.get(url, data, function(data, status){\n        const timeframeSelect = $(\"#timeframesSelect\");\n        timeframeSelect.empty(); // remove old options\n        const timeframeSelectBox = timeframeSelect[0];\n        $.each(data, function(key,value) {\n            timeframeSelectBox.append(new Option(value,value));\n        });\n        timeframeSelect.trigger('change');\n    });\n}\n\nfunction check_date_input(){\n    const startDate = new Date($(\"#startDate\").val());\n    const enddate = new Date($(\"#endDate\").val());\n    const startDateMax = new Date( $(\"#startDate\")[0].max);\n    const endDateMin = new Date( $(\"#endDate\")[0].min);\n    if(isNaN(startDate) && isNaN(enddate)){\n        return true;\n    }else if (!isNaN(enddate) && isNaN(startDate)){\n        create_alert(\"error\", \"You should specify a start date.\", \"\");\n        return false;\n    }else if((!isNaN(startDate) && startDate > startDateMax) || (!isNaN(enddate) && enddate < endDateMin)){\n        create_alert(\"error\", \"Invalid date range.\", \"\");\n        return false;\n    }else{\n        return true;\n    }\n}\nfunction is_full_candle_history_exchanges(){\n    const full_history_exchanges = $('#exchangeSelect > optgroup')[0].children;\n    const selected_exchange = $('#exchangeSelect').find(\":selected\")[0];\n    return $.inArray(selected_exchange, full_history_exchanges) !== -1;\n}\n\nlet dataFilesTable = $('#dataFilesTable').DataTable({\"order\": [[ 1, 'desc' ]]});\n\n\nfunction handleSelects(){\n    createSelect2();\n    $('#exchangeSelect').on('change', function() {\n        update_symbol_list($('#symbolsSelect').attr(update_url_attr), $('#exchangeSelect').val());\n        update_available_timeframes_list($('#timeframesSelect').attr(update_url_attr), $('#exchangeSelect').val());\n        is_full_candle_history_exchanges() ? $(\"#collector_date_range\").show() : $(\"#collector_date_range\").hide();\n    });\n    $('#collect_data').click(function(){\n        if(check_date_input()){\n            start_collector();\n        }\n    });\n    $('#stop_collect_data').click(function(){\n        stop_collector();\n    });\n    $('#inputFile').on('change',function(){\n        handle_file_selection();\n    });\n    $(\"#endDate\").on('change', function(){\n        let endDate = new Date(this.value);\n        if(!isNaN(endDate)){\n            const endDateMax = new Date();\n            endDateMax.setDate(endDateMax.getDate() - 1);\n            endDate.setDate(endDate.getDate() - 1);\n            if(endDate > endDateMax){\n                this.value = endDateMax.toISOString().split(\"T\")[0];\n                endDate = endDateMax;\n            }\n            $(\"#startDate\")[0].max = endDate.toISOString().split(\"T\")[0];\n        }\n    });\n    $(\"#startDate\").on('change', function(){\n        const startDate = new Date(this.value);\n        if(!isNaN(startDate)){\n            const startDateMax = new Date();\n            startDateMax.setDate(startDateMax.getDate() - 2);\n            startDate.setDate(startDate.getDate() + 1);\n            $(\"#endDate\")[0].min = startDate.toISOString().split(\"T\")[0];\n        }\n    });\n\n    const endDateMax = new Date();\n    endDateMax.setDate(endDateMax.getDate() - 1);\n    $(\"#endDate\")[0].max = endDateMax.toISOString().split(\"T\")[0];\n    const startDateMax = new Date();\n    startDateMax.setDate(startDateMax.getDate() - 2);\n    $(\"#startDate\")[0].max = startDateMax.toISOString().split(\"T\")[0];\n}\n\n\nfunction createSelect2(){\n    $(\"#symbolsSelect\").select2({\n        closeOnSelect: false,\n        placeholder: \"Symbol\"\n    });\n    $(\"#timeframesSelect\").select2({\n        closeOnSelect: false,\n        placeholder: \"All Timeframes\"\n    });\n}\n\n\n$(document).ready(function() {\n    handle_data_files_buttons();\n    is_full_candle_history_exchanges() ? $(\"#collector_date_range\").show() : $(\"#collector_date_range\").hide();\n    $('#importFileButton').attr('disabled', true);\n    dataFilesTable.on(\"draw.dt\", function(){\n        handle_data_files_buttons();\n    });\n    handleSelects();\n    DataCollectorDoneCallbacks.push(reload_table);\n    init_data_collector_status_websocket();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/dsl_help.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    const fetchDSLKeywordsIfPossible = async () => {\n        const dslTableBody = $(\"#dsl-keywords-table-body\");\n        if(dslTableBody.length === 0 || dslTableBody.length === 0){\n            return;\n        }\n        const url = dslTableBody.data(\"update-url\");\n        const response = await async_send_and_interpret_bot_update(undefined, url, null, \"GET\");\n        response.forEach(keywordData => {\n            dslTableBody.append(`<tr><td>${keywordData.name}</td><td>${keywordData.description}</td><td>${keywordData.example}</td><td>${keywordData.type}</td></tr>`);\n        });\n        $(\"#dsl-keywords-table\").DataTable({\n            \"pageLength\": 50,\n            \"order\": [[ 3, \"desc\" ], [ 0, \"asc\" ]],\n        });\n    }\n    fetchDSLKeywordsIfPossible();\n});"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/evaluator_configuration.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction get_tabs_config(){\n    return $(document).find(\".\" + config_root_class + \" .\" + config_container_class);\n}\n\nfunction handle_reset_buttons(){\n    $(\"#reset-config\").click(function() {\n        reset_configuration_element();\n    })\n}\n\nfunction something_is_unsaved(){\n\n    const config_root = $(\"#super-container\");\n    return (\n        config_root.find(\".\"+card_class_modified).length > 0\n            || config_root.find(\".\"+deck_container_modified_class).length > 0\n            || config_root.find(\".\"+primary_badge).length > 0\n    )\n}\n\nfunction parse_new_value(element){\n    const raw_data = replace_spaces(replace_break_line(element.text()));\n\n    // simple case\n    if(element[0].hasAttribute(current_value_attr)){\n        const value = replace_spaces(replace_break_line(element.attr(current_value_attr)));\n        if(element[0].hasAttribute(config_data_type_attr)){\n            switch(element.attr(config_data_type_attr)) {\n                case \"bool\":\n                    return value === true || value === \"true\";\n                case \"number\":\n                    return Number(value);\n                default:\n                    return value;\n            }\n        }else{\n            return value;\n        }\n    }\n    // with data type\n    else if(element[0].hasAttribute(config_data_type_attr)){\n        switch(element.attr(config_data_type_attr)) {\n            case \"bool\":\n                return element.is(\":checked\");\n            case \"list\":\n                const new_value = [];\n                element.find(\":selected\").each(function(index, value){\n                    new_value.splice(index, 0, replace_spaces(replace_break_line(value.text)));\n                });\n                return new_value;\n            case \"number\":\n                return Number(raw_data);\n            default:\n                return raw_data;\n        }\n\n    // without information\n    }else{\n        return raw_data;\n    }\n}\n\nfunction _save_eval_config(element, restart_after_save){\n    const full_config = $(\"#super-container\");\n    const updated_config = {};\n    const update_url = element.attr(update_url_attr);\n\n    // take all tabs into account\n    get_tabs_config().each(function(){\n        $(this).find(\".\"+config_element_class).each(function(){\n            const config_type = $(this).attr(config_type_attr);\n\n            if(!(config_type in updated_config)){\n                updated_config[config_type] = {};\n            }\n\n            const new_value = parse_new_value($(this));\n            const config_key = get_config_key($(this));\n\n            if(get_config_value_changed($(this), new_value, config_key)){\n                updated_config[config_type][config_key] = new_value;\n            }\n        })\n    });\n\n    updated_config[\"restart_after_save\"] = restart_after_save;\n\n    // send update\n    send_and_interpret_bot_update(updated_config, update_url, full_config, handle_save_buttons_success_callback);\n}\n\nfunction handle_save_buttons(){\n    $(\"#save-config\").click(function() {\n        _save_eval_config($(this), false);\n    })\n    $(\"#save-config-and-restart\").click(function() {\n        _save_eval_config($(this), true);\n    })\n}\n\nfunction get_config_key(elem){\n    return elem.attr(config_key_attr);\n}\n\nfunction get_config_value_changed(element, new_value, config_key) {\n    let new_value_str = new_value.toString();\n    if(new_value instanceof Array && new_value.length > 0){\n        //need to format array to match python string representation of config\n        var str_array = [];\n        $.each(new_value, function(i, val) {\n            str_array.push(\"'\"+val+\"'\");\n        });\n        new_value_str = \"[\" + str_array.join(\", \") + \"]\";\n    }\n    return get_value_changed(new_value_str, element.attr(config_value_attr), config_key);\n}\n\nfunction get_value_changed(new_val, dom_conf_val, config_key){\n    const lower_case_val = new_val.toLowerCase();\n    if(new_val.toLowerCase() !== dom_conf_val.toLowerCase()){\n        return true;\n    }else if (config_key in validated_updated_global_config){\n        return lower_case_val !== validated_updated_global_config[config_key].toString().toLowerCase();\n    }else{\n        return false;\n    }\n\n}\n\nfunction handle_save_buttons_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    update_dom(dom_root_element, msg);\n    create_alert(\"success\", \"Configuration successfully updated\", \"Restart OctoBot for changes to be applied.\");\n}\n\nfunction handle_evaluator_configuration_editor(){\n    $(\".config-element\").click(function(e){\n        if (isDefined($(e.target).attr(no_activation_click_attr))){\n            // do not trigger when click on items with no_activation_click_attr set\n            return;\n        }\n        const element = $(this);\n\n        if (element.hasClass(config_element_class)){\n\n            if (element[0].hasAttribute(config_type_attr) && element.attr(config_type_attr) === evaluator_config_type){\n\n                // build data update\n                let new_value = parse_new_value(element);\n                let current_value;\n\n                try {\n                    current_value = element.attr(current_value_attr).toLowerCase();\n                }\n                catch(e) {\n                    current_value = element.attr(current_value_attr);\n                }\n\n                // todo\n                if (current_value === \"true\"){\n                    new_value = \"false\";\n                }else if(current_value === \"false\"){\n                    new_value = \"true\";\n                }\n\n                // update current value\n                element.attr(current_value_attr, new_value);\n\n                //update dom\n                update_element_temporary_look(element);\n            }\n        }\n    });\n}\n\nfunction reset_configuration_element(){\n    remove_exit_confirm_function();\n    location.reload();\n}\n\nlet validated_updated_global_config = {};\n\n$(document).ready(function() {\n    handle_reset_buttons();\n    handle_save_buttons();\n\n    handle_evaluator_configuration_editor();\n\n    register_exit_confirm_function(something_is_unsaved);\n});"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/extensions.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    const showModalIfNecessary = () => {\n        $(\".modal\").each((_, element) => {\n            const jqueryelement = $(element);\n            if (jqueryelement.data(\"show-by-default\") == \"True\") {\n                jqueryelement.modal();\n            }\n        })\n    }\n\n    const handlePaymentWaiter = async () => {\n        const waiterModal = $(\"#waiting-for-owned-packages-to-install-modal\");\n        if(waiterModal && waiterModal.data(\"show-by-default\") == \"True\"){\n            const url = waiterModal.data(\"url\");\n            let hasExtension = false;\n            while (!hasExtension){\n                const has_open_source_package_resp = await async_send_and_interpret_bot_update(null, url, null)\n                if(has_open_source_package_resp.has_open_source_package){\n                    hasExtension = true\n                    document.location.href = window.location.href.replace(\"&loop=true\", \"\").replace(\"?refresh_packages=true\", \"\");\n                } else {\n                    await new Promise(r => setTimeout(r, 3000));\n                }\n            }\n        }\n    }\n\n    const registerTriggerCheckout = () => {\n        $(\"button[data-role=\\\"open-package-purchase\\\"]\").click(() => {\n            $(\"#select-payment-method-modal\").modal();\n        })\n        $(\"button[data-role=\\\"restart\\\"]\").click(() => {\n            $(\"#select-payment-method-modal\").modal();\n        })\n        $(\"button[data-role=\\\"open-checkout\\\"]\").click(async (event) => {\n            const button = $(event.currentTarget);\n            const checkoutButtons = $(\"button[data-role=\\\"open-checkout\\\"]\");\n            const origin_val = button.text();\n            const paymentMethod = button.data(\"payment-method\")\n            const url = button.data(\"checkout-api-url\")\n            const data = {\n                paymentMethod: paymentMethod,\n                redirectUrl: `${window.location.href}?refresh_packages=true&loop=true`\n            }\n            let fetchedCheckoutUrl = null;\n            try {\n                checkoutButtons.addClass(\"disabled\");\n                button.html(\"<i class='fa fa-circle-notch fa-spin'></i> Loading checkout\");\n                const checkoutUrl = await async_send_and_interpret_bot_update(data, url, null)\n                if(checkoutUrl.url === null){\n                    create_alert(\"success\", \"User already owns this extension\", \"\");\n                } else {\n                    fetchedCheckoutUrl = checkoutUrl.url;\n                    $(\"p[data-role=\\\"checkout-url-fallback-part\\\"]\").removeClass(\"d-none\");\n                    const checkoutUrlFallbackLink = $(\"a[data-role=\\\"checkout-url-fallback\\\"]\");\n                    checkoutUrlFallbackLink.attr(\"href\", fetchedCheckoutUrl);\n                    checkoutUrlFallbackLink.text(fetchedCheckoutUrl);\n                    document.location.href = fetchedCheckoutUrl;\n                }\n            } finally {\n                if(fetchedCheckoutUrl === null){\n                    checkoutButtons.removeClass(\"disabled\");\n                }\n                button.html(origin_val);\n            }\n        })\n    }\n\n    showModalIfNecessary();\n    registerTriggerCheckout();\n    handlePaymentWaiter();\n});"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/logs.js",
    "content": "function handleLogsExporter(){\n    trigger_file_downloader_on_click($(\".export-logs-button\"));\n}\n\n$(document).ready(function() {\n    $('#logs_datatable').DataTable({\n      // order by time: most recent first\n      \"order\": [[ 0, \"desc\" ]]\n    });\n    $('#notifications_datatable').DataTable({\n      // order by time: most recent first\n      \"order\": [[ 0, \"desc\" ]]\n    });\n    handleLogsExporter();\n});"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/market_status.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\nfunction get_in_backtesting_mode() {\n    return $(\"#symbol_graph\").attr(\"backtesting_mode\") === \"True\";\n}\n\n\nfunction init_update_handler(){\n    socket.on(\"candle_graph_update_data\", function (data) {\n        if(!cancel_next_update){\n            updating_graph = true;\n            update_graph(graph.attr(\"exchange\"), true, data.data);\n        }else{\n            cancel_next_update = false;\n        }\n    });\n    socket.on('new_data', function (data) {\n        if(!cancel_next_update) {\n            updating_graph = true;\n            update_graph(graph.attr(\"exchange\"), true, data.data, false);\n        }\n    });\n}\n\nfunction schedule_update(){\n    setTimeout(function () {\n        socket.emit(\"candle_graph_update\", update_details);\n    }, price_graph_update_interval)\n}\n\n\nfunction update_graph(exchange, update=false, data=undefined, re_update=true, initialization=false){\n    const in_backtesting = get_in_backtesting_mode();\n    if(isDefined(update_details.time_frame) && isDefined(update_details.symbol) && isDefined(exchange)){\n        const formated_symbol = update_details.symbol.replace(new RegExp(\"/\",\"g\"), \"|\");\n        if(isDefined(data) && (formated_symbol !== data.symbol.replace(new RegExp(\"/\",\"g\"), \"|\") ||\n            update_details.exchange_id !== data.exchange_id)){\n            return;\n        }\n        if (initialization && !in_backtesting){\n            init_update_handler();\n            updating_graph = false;\n        }\n        const valid_exchange_name = exchange.split(\"[\")[0];\n        get_symbol_price_graph(\"graph-symbol-price\", update_details.exchange_id, valid_exchange_name,\n            formated_symbol, update_details.time_frame, true, in_backtesting, !update,\n            true, 0, data, schedule_update);\n        if (update && re_update && !in_backtesting){\n            schedule_update();\n        }\n    }else{\n        const loadingSelector = $(\"div[name='loadingSpinner']\");\n        if (loadingSelector.length) {\n            loadingSelector.addClass(hidden_class);\n        }\n        $(\"#graph-symbol-price\").html(\"<h7>Impossible to display price graph, if this error keeps appearing, \" +\n            \"go to back to <strong>Trading</strong> and re-display this page.</h7>\")\n    }\n}\n\nfunction change_time_frame(new_time_frame) {\n    update_details.time_frame = new_time_frame;\n    update_graph(graph.attr(\"exchange\"));\n}\n\nconst graph = $(\"#symbol_graph\");\nconst timeFrameSelect = $(\"#time-frame-select\");\nconst update_details = {\n    exchange_id: graph.attr(\"exchange_id\"),\n    symbol: graph.attr(\"symbol\"),\n    time_frame: timeFrameSelect.val()\n};\nlet updating_graph = false;\nlet cancel_next_update = false;\n\nconst socket = get_websocket(\"/dashboard\");\n\n$(document).ready(function() {\n    update_graph(graph.attr(\"exchange\"), false, undefined, true, true);\n    timeFrameSelect.on('change', function () {\n        const new_val = this.value;\n        cancel_next_update = true;\n        if(updating_graph){\n            setTimeout(function () {\n                change_time_frame(new_val)\n            }, 50);\n        }else{\n            change_time_frame(new_val);\n        }\n    });\n\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/navbar.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction trigger_trader_state(element) {\n    let updated_config = {};\n    const update_url = element.attr(update_url_attr);\n    const config_key = element.attr(config_key_attr);\n    const config_type = element.attr(config_type_attr);\n    const set_to_activated = element.attr(current_value_attr).toLowerCase() === \"true\";\n\n    if (config_key === \"trader_enabled\") {\n        updated_config = {\n            [config_type]: {\n                \"trader_enabled\": set_to_activated,\n                \"trader-simulator_enabled\": !set_to_activated,\n            }\n        }\n    } else {\n        updated_config = {\n            [config_type]: {\n                \"trader_enabled\": !set_to_activated,\n                \"trader-simulator_enabled\": set_to_activated,\n            }\n        }\n    }\n\n    updated_config[\"restart_after_save\"] = true;\n\n    // send update\n\n    function post_trading_state_update_success_callback(updated_data, update_url, dom_root_element, msg, status){\n        create_alert(\"success\", \"Trader switched\" , \"\");\n        hideTradingStateModal()\n    }\n\n    function post_trading_state_update_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n        create_alert(\"error\", \"Error when switching trader : \"+result.responseText, \"\");\n        hideTradingStateModal()\n    }\n    send_and_interpret_bot_update(updated_config, update_url, null, post_trading_state_update_success_callback, post_trading_state_update_error_callback);\n}\n\nfunction displayTradingStateModal() {\n    showModalIfAny($(\"#tradingSwitchModal\"))\n}\n\nfunction hideTradingStateModal() {\n    hideModalIfAny($(\"#tradingSwitchModal\"))\n}\n\n$(document).ready(function() {\n    $(\"#switchTradingState\").click(function(){\n        displayTradingStateModal()\n    });\n    $(\".trading-mode-switch-button\").click(function(){\n        trigger_trader_state($(this))\n    });\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/portfolio.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n\n    const createPortfolioChart = (element_id, title, update) => {\n        const data = {};\n        const element = $(`#${element_id}`);\n        const max_medium_screen_legend_items = 50;\n        const max_mobile_legend_items = 6;\n        let at_least_one_value = false;\n        let displayLegend = true;\n        let graphHeight = element.attr(\"data-md-height\");\n        if(isMobileDisplay()){\n            graphHeight = element.attr(\"data-sm-height\");\n        }\n        element.attr(\"height\", graphHeight);\n\n        $(\".symbol-holding\").each(function (){\n            const total_value = $(this).find(\".total-value\").text();\n            if($.isNumeric(total_value)){\n                data[$(this).find(\".symbol\").text()] = Number(total_value);\n                if(Number(total_value) > 0 ){\n                    at_least_one_value = true;\n                }\n            }\n        });\n        const dataLength = Object.keys(data).length;\n        // display graph only if at least one value is available\n        if(at_least_one_value && dataLength > 0 && element.length > 0){\n            if(isMobileDisplay() && dataLength > max_mobile_legend_items){\n                // legend is hiding the chart on smaller displays if too many elements are present\n                displayLegend = false;\n            }else if(dataLength > max_medium_screen_legend_items){\n                // legend is hiding the chart on if too many elements are present\n                displayLegend = false;\n            }\n            create_doughnut_chart(element[0], data, title, displayLegend, graphHeight, update);\n        }else{\n            element.addClass(hidden_class);\n        }\n    }\n\n    const handle_portfolio_button = () => {\n        const refreshButton = $(\"#refresh-portfolio\");\n        if(refreshButton){\n            refreshButton.click(function () {\n                const update_url = refreshButton.attr(update_url_attr);\n                send_and_interpret_bot_update({}, update_url, null, generic_request_success_callback, generic_request_failure_callback);\n            });\n        }\n    }\n\n    const start_periodic_refresh = () => {\n        setInterval(function() {\n            $(\"#portfolio-display\").load(location.href + \" #portfolio-display\", function (){\n                update_display(true, true);\n            });\n        }, portfolio_update_interval);\n    }\n\n    const displayPortfolioTable = () => {\n        handle_rounded_numbers_display();\n        ordersDataTable = $('#holdings-table').DataTable({\n            \"paging\": false,\n            \"bDestroy\": true,\n            \"order\": [[ 2, \"desc\" ]],\n            \"searching\": $(\"tr.symbol-holding\").length > 10,\n        });\n    }\n\n    const displayPortfolioContent = (referenceMarket, update) => {\n        displayPortfolioTable();\n        const chartTitle = `Assets value (${referenceMarket})`;\n        createPortfolioChart(\"portfolio_doughnutChart\", chartTitle, update);\n        handleButtons();\n    }\n\n    const update_display = (withImages, update) => {\n        const referenceMarket = $(\"#portfoliosCard\").attr(\"reference_market\");\n        displayPortfolioContent(referenceMarket, update);\n        if(withImages){\n            handleDefaultImages();\n        }\n    }\n\n    let firstLoad = true;\n    const handleClearButton = () => {\n        $(\"#clear-portfolio-history-button\").on(\"click\", (event) => {\n            if (confirm(\"Clear portfolio history ?\") === false) {\n                return false;\n            }\n            const url = $(event.currentTarget).data(\"url\")\n            const success = (updated_data, update_url, dom_root_element, msg, status) => {\n                // reload page on success\n                location.reload();\n            }\n            send_and_interpret_bot_update(null, url, null, success, generic_request_failure_callback)\n        })\n    }\n\n    const handleButtons = () => {\n        handle_portfolio_button();\n        handleClearButton()\n    }\n    update_display(false, false);\n    if(firstLoad){\n        start_periodic_refresh();\n    }\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/profile_management.js",
    "content": "function handleProfileActivator(){\n    const profileActivatorButton = $(\".activate-profile-button\");\n    if (profileActivatorButton.length){\n        profileActivatorButton.click(function (){\n            const changeProfileURL = $(this).attr(\"data-url\");\n            window.location.replace(changeProfileURL);\n        });\n    }\n}\n\nfunction onProfileEdit(isEditing, profileSave){\n    // disable global save config button to avoid save buttons confusion\n    $(\"#save-config\").attr(\"disabled\", isEditing);\n    profileSave.attr(\"disabled\", !isEditing);\n}\n\nfunction handleProfileEditor(){\n    const saveProfile = $(\".save-profile\");\n    const profileName = $('.profile-name-editor');\n    const profileDescription = $('.profile-description-editor');\n    const profileComplexity = $('.profile-complexity-selector');\n    const profileRisk = $('.profile-risk-selector');\n    profileName.on('save', function (){\n        onProfileEdit(true, $(this).parents(\".profile-details\").find(\".save-profile\"));\n    });\n    profileDescription.on('save', function (){\n        onProfileEdit(true, $(this).parents(\".profile-details\").find(\".save-profile\"));\n    });\n    profileComplexity.on('change', function (){\n        onProfileEdit(true, $(this).parents(\".profile-details\").find(\".save-profile\"));\n    });\n    profileRisk.on('change', function (){\n        onProfileEdit(true, $(this).parents(\".profile-details\").find(\".save-profile\"));\n    });\n    saveProfile.click(function (){\n        onProfileEdit(false, $(this));\n        $(this).tooltip(\"hide\");\n        const updateURL = $(this).attr(\"data-url\");\n        const profileDetails = $(this).parents(\".profile-details\");\n        const data = {\n            id: profileDetails.attr(\"data-id\"),\n            name: profileDetails.find(\".profile-name-editor\").editable(\"getValue\", true),\n            description: profileDetails.find(\".profile-description-editor\").editable(\"getValue\", true),\n            complexity: profileDetails.find(\".profile-complexity-selector\").val(),\n            risk: profileDetails.find(\".profile-risk-selector\").val(),\n        };\n        send_and_interpret_bot_update(data, updateURL, null,\n            saveCurrentProfileSuccessCallback, saveCurrentProfileFailureCallback);\n    });\n}\n\nfunction saveCurrentProfileSuccessCallback(updated_data, update_url, dom_root_element, msg, status){\n    create_alert(\"success\", \"Profile updated\");\n    $(\"[data-role=profile-name]\").each(function (){\n        const profileIdAttr = $(this).attr(\"data-profile-id\");\n        if(typeof profileIdAttr === \"undefined\" || profileIdAttr === updated_data[\"id\"]){\n            $(this).html(updated_data[\"name\"]);\n        }\n    });\n}\n\n\nfunction saveCurrentProfileFailureCallback(updated_data, update_url, dom_root_element, msg, status) {\n    $(\"#save-current-profile\").attr(\"disabled\", false);\n    create_alert(\"error\", msg.responseText, \"\");\n}\n\nfunction handleProfileCreator(){\n    const createButton = $(\".duplicate-profile\");\n    if(createButton.length){\n        createButton.click(function (){\n            send_and_interpret_bot_update({}, $(this).attr(\"data-url\"), null,\n                profileActionSuccessCallback, profileActionFailureCallback);\n        });\n    }\n}\n\nfunction profileActionSuccessCallback(updated_data, update_url, dom_root_element, msg, status){\n    location.reload();\n}\n\n\nfunction profileActionFailureCallback(updated_data, update_url, dom_root_element, msg, status) {\n    create_alert(\"error\", msg.responseText, \"\");\n}\n\nfunction handleProfileImporter(){\n    const importForm = $(\".profile-import-form\");\n    const importButton = $(\".import-profile-button\");\n    const profileInput = $(\".profile-input\");\n    if(importForm.length && importButton.length && profileInput.length){\n        importButton.click(function () {\n            $(this).siblings(\".profile-import-form\").find(\".profile-input\").click();\n        });\n        profileInput.on(\"change\", function () {\n            $(this).parents(\".profile-import-form\").submit();\n        });\n    }\n}\n\nfunction handleProfileDownloader(){\n    const downloadForm = $(\".profile-download-form\");\n    const importButton = downloadForm.find('button[data-role=\"download-profile-button\"]');\n    const profileInput = $(\"#inputProfileLink\");\n    if(importButton.length && profileInput.length){\n        importButton.click(function () {\n            if($(\"#inputProfileLink\").val()){\n                $(this).parents(\".profile-download-form\").submit();\n            }\n        });\n    }\n}\n\nfunction handleProfileExporter(){\n    trigger_file_downloader_on_click($(\".export-profile-button\"));\n}\n\nfunction selectCurrentProfile(profileNameDisplay){\n    $(\"#profilesSubmenu\").collapse(\"show\");\n    const profileId = profileNameDisplay.attr(\"data-profile-id\");\n    activate_tab($(`#profile-${profileId}-tab`));\n}\n\nfunction handleProfileSelector(){\n    const profileNameDisplay = $(\"a[data-role=current-profile-selector]\");\n    profileNameDisplay.click(function (){\n        selectCurrentProfile(profileNameDisplay);\n    });\n    $(\"[data-role=current-profile-selector]\").click(function (){\n        selectCurrentProfile(profileNameDisplay);\n    });\n}\n\nfunction handleProfileRemover(){\n    const removeProfileButton = $(\".remove-profile-button\");\n    if(removeProfileButton.length){\n        removeProfileButton.click(function (){\n            if (confirm(\"Delete this profile ?\")) {\n                const data = {id: $(this).attr(\"data-profile-id\")};\n                send_and_interpret_bot_update(data, $(this).attr(\"data-url\"), null,\n                    profileActionSuccessCallback, profileActionFailureCallback);\n            }\n        });\n    }\n}\n\n$(document).ready(function() {\n    handleProfileActivator();\n    handleProfileSelector();\n    handleProfileEditor();\n    handleProfileCreator();\n    handleProfileImporter();\n    handleProfileDownloader();\n    handleProfileExporter();\n    handleProfileRemover();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/profiles_selector.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    // for some reason this is not always working when leaving it to bootstrap\n    const ensureModals = () => {\n        $('button[data-toggle=\"modal\"]').each((_, element) => {\n            $(element).click((event) => {\n                const events = jQuery._data(event.currentTarget, \"events\" )\n                // One event means bootstrap did not register this click event\n                if(typeof events !== \"undefined\" && typeof events.click !== \"undefined\"\n                    && events.click.length === 1){\n                    const element = $(event.currentTarget);\n                    element.parent().children(element.data(\"target\")).modal();\n                }\n            })\n        })\n    }\n\n    ensureModals();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/strategy_optimizer.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction recompute_nb_iterations(){\n    const nb_eval_iter = Math.pow(2, $(\"#evaluatorsSelect\").find(\":selected\").length)-1;\n    const nb_tf_iter = Math.pow(2, $(\"#timeFramesSelect\").find(\":selected\").length)-1;\n    const nb_selected = $(\"#risksSelect\").find(\":selected\").length*nb_eval_iter*nb_tf_iter;\n    $(\"#numberOfSimulatons\").text(nb_selected);\n}\n\nfunction check_disabled(lock=false){\n    if(lock){\n        $(\"#startOptimizer\").prop('disabled', true);\n    }\n    else if($(\"#strategySelect\").find(\":selected\").length > 0 && $(\"#risksSelect\").find(\":selected\").length  > 0 &&\n        $(\"#timeFramesSelect\").find(\":selected\").length > 0 && $(\"#evaluatorsSelect\").find(\":selected\").length  > 0){\n        $(\"#startOptimizer\").prop('disabled', false);\n    }else{\n        $(\"#startOptimizer\").prop('disabled', true);\n    }\n}\n\nfunction start_optimizer(source){\n    $(\"#progess_bar\").show();\n    $(\"#progess_bar_anim\").css('width', '0%').attr(\"aria-valuenow\", '0');\n    source.prop('disabled', true);\n    const update_url = source.attr(update_url_attr);\n    const data = {};\n    data[\"strategy\"]=get_selected_options($(\"#strategySelect\"));\n    data[\"time_frames\"]=get_selected_options($(\"#timeFramesSelect\"));\n    data[\"evaluators\"]=get_selected_options($(\"#evaluatorsSelect\"));\n    data[\"risks\"]=get_selected_options($(\"#risksSelect\"));\n    send_and_interpret_bot_update(data, update_url, source, start_optimizer_success_callback, start_optimizer_error_callback);\n}\n\nfunction lock_inputs(lock=true){\n    const disabled_attr = 'disabled';\n    if ( $(\"#strategySelect\").prop(disabled_attr) !== lock){\n        $(\"#strategySelect\").prop(disabled_attr, lock);\n    }\n    if ( $(\"#timeFramesSelect\").prop(disabled_attr) !== lock){\n        $(\"#timeFramesSelect\").prop(disabled_attr, lock);\n    }\n    if ( $(\"#evaluatorsSelect\").prop(disabled_attr) !== lock){\n        $(\"#evaluatorsSelect\").prop(disabled_attr, lock);\n    }\n    if ( $(\"#risksSelect\").prop(disabled_attr) !== lock){\n        $(\"#risksSelect\").prop(disabled_attr, lock);\n    }\n    if(!($(\"#progess_bar\").is(\":visible\")) && lock){\n        $(\"#progess_bar_anim\").css('width', '0%').attr(\"aria-valuenow\", '0');\n        $(\"#progess_bar\").show();\n    }\n    else if (!lock){\n        $(\"#progess_bar_anim\").css('width', '100%').attr(\"aria-valuenow\", '100');\n        $(\"#progess_bar\").hide();\n    }\n    check_disabled(lock);\n\n}\n\nfunction start_optimizer_success_callback(data, update_url, source, msg, status){\n    create_alert(\"success\", msg, \"\");\n    lock_inputs();\n}\n\nfunction start_optimizer_error_callback(data, update_url, source, result, status, error){\n    source.prop('disabled', false);\n    $(\"#progess_bar\").hide();\n    create_alert(\"error\", \"Error when starting optimizer: \"+result.responseText, \"\");\n}\n\nfunction populate_select(element, options){\n    element.empty(); // remove old options\n    $.each(options, function(key, value) {\n        if (key === 0){\n            element.append($('<option selected = \"selected\" value=\"' + value + '\" ></option>').attr(\"value\", value).text(value));\n        }else{\n            element.append($('<option value=\"' + value + '\" ></option>').attr(\"value\", value).text(value));\n        }\n    });\n}\n\nfunction update_strategy_params(url, strategy){\n    var data = {strategy_name: strategy};\n    $.get(url, data, function(data, status){\n        populate_select($(\"#evaluatorsSelect\"), data[\"evaluators\"]);\n        populate_select($(\"#timeFramesSelect\"), data[\"time_frames\"]);\n    });\n}\n\nfunction updateOptimizerProgress(progress, overall_progress){\n    $(\"#progess_bar_anim\").css('width', progress+'%').attr(\"aria-valuenow\", progress);\n\n    const nb_progress = Number(overall_progress);\n\n    if(isDefined(progressChart)){\n        update_circular_progress_doughnut(progressChart, nb_progress, 100 - nb_progress);\n        $(\"#optimize_doughnutChart_progress\").html(nb_progress.toString()+\"%\");\n    }\n}\n\nfunction check_optimizer_state(socket){\n    socket.emit(\"strategy_optimizer_status\");\n}\n\nfunction handle_optimizer_state_update(data){\n    const status = data[\"status\"];\n    const progress = data[\"progress\"];\n    const overall_progress = data[\"overall_progress\"];\n    const errors = data[\"errors\"];\n    const error_div = $(\"#error_info\");\n    const error_text_div = $(\"#error_info_text\");\n    const report_datatable_card = $(\"#report_datatable_card\");\n    const has_errors = errors !== null;\n    let alert_type = \"success\";\n    let alert_additional_text = \"Strategy optimized finished simulations.\";\n    if(has_errors){\n        error_text_div.text(errors);\n        error_div.show();\n        alert_type = \"error\";\n        alert_additional_text = \"Strategy optimized finished simulations with error(s).\"\n    }else{\n        error_text_div.text(\"\");\n        error_div.hide();\n    }\n    if(status === \"computing\"){\n        lock_inputs();\n        updateOptimizerProgress(progress, overall_progress);\n        first_refresh_state = status;\n        if(report_datatable_card.is(\":visible\")){\n            report_datatable_card.hide();\n            reportTable.clear();\n        }\n    }\n    else{\n        lock_inputs(false);\n        if(status === \"finished\"){\n            if(!report_datatable_card.is(\":visible\")){\n                report_datatable_card.show();\n            }\n            if(reportTable.rows().count() === 0){\n                reportTable.ajax.reload( null, false);\n            }\n            if((first_refresh_state !== \"\" || has_errors) && first_refresh_state !== \"finished\"){\n                create_alert(alert_type, alert_additional_text, \"\");\n                first_refresh_state=\"finished\";\n            }\n        }\n    }\n    if(first_refresh_state === \"\"){\n        first_refresh_state = status;\n    }\n}\n\nconst iterationColumnsDef = [\n    {\n        \"title\": \"#\",\n        \"targets\": 0,\n        \"data\": \"id\",\n        \"name\": \"id\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Evaluator(s)\",\n        \"targets\": 1,\n        \"data\": \"evaluators\",\n        \"name\": \"evaluators\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Time Frame(s)\",\n        \"targets\": 2,\n        \"data\": \"time_frames\",\n        \"name\": \"time_frames\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Risk\",\n        \"targets\": 3,\n        \"data\": \"risk\",\n        \"name\": \"risk\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Average trades count\",\n        \"targets\": 4,\n        \"data\": \"average_trades\",\n        \"name\": \"average_trades\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Score: the higher the better\",\n        \"targets\": 5,\n        \"data\": \"score\",\n        \"name\": \"score\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    }\n];\n\nconst reportColumnsDef = [\n    {\n        \"title\": \"#\",\n        \"targets\": 0,\n        \"data\": \"id\",\n        \"name\": \"id\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Evaluator(s)\",\n        \"targets\": 1,\n        \"data\": \"evaluators\",\n        \"name\": \"evaluators\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Risk\",\n        \"targets\": 2,\n        \"data\": \"risk\",\n        \"name\": \"risk\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Average trades count\",\n        \"targets\": 3,\n        \"data\": \"average_trades\",\n        \"name\": \"average_trades\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    },\n    {\n        \"title\": \"Comparative score: the lower the better\",\n        \"targets\": 4,\n        \"data\": \"score\",\n        \"name\": \"score\",\n        \"render\": function(data, type, row, meta){\n            return data;\n        }\n    }\n];\nlet first_refresh_state = \"\";\n\nconst progressChart = create_circular_progress_doughnut($(\"#optimize_doughnutChart\")[0]);\n\nfunction init_websocket(){\n    const socket = get_websocket(\"/strategy_optimizer\");\n    socket.on(\"strategy_optimizer_status\", function (data) {\n        handle_optimizer_state_update(data);\n    });\n    return socket;\n}\n\nfunction init_data_tables_and_refreshers(){\n    reportTable = $(\"#report_datatable\").DataTable({\n        ajax: {\n            \"url\": $(\"#report_datatable\").attr(update_url_attr),\n            \"dataSrc\": \"\"\n        },\n        deferRender: true,\n        autoWidth: true,\n        autoFill: true,\n        columnDefs: reportColumnsDef\n    });\n\n    const iterationTable = $(\"#results_datatable\").DataTable({\n        ajax: {\n            \"url\": $(\"#results_datatable\").attr(update_url_attr),\n            \"dataSrc\": \"\"\n        },\n        deferRender: true,\n        autoWidth: true,\n        autoFill: true,\n        columnDefs: iterationColumnsDef\n    });\n\n    const socket = init_websocket();\n\n    setInterval(function(){refresh_message_table(iterationTable);}, 1500);\n    function refresh_message_table(iterationTable){\n        iterationTable.ajax.reload( null, false );\n        if(iterationTable.rows().count() > 0){\n            $(\"#results_datatable_card\").show();\n        }\n        check_optimizer_state(socket);\n    }\n}\n\nfunction register_events(){\n    $('#strategySelect').on('input', function() {\n        update_strategy_params($('#strategySelect').attr(update_url_attr), $('#strategySelect').val());\n    });\n\n    $(\".multi-select-element\").select2({\n        dropdownAutoWidth : true,\n        multiple: true,\n        closeOnSelect: false\n    });\n    $(\".multi-select-element\").on('change', function (e) {\n        recompute_nb_iterations();\n        check_disabled();\n    });\n    $(\"#startOptimizer\").click(function(){\n        start_optimizer($(this));\n    });\n}\n\nlet reportTable = undefined;\n\n$(document).ready(function() {\n    check_disabled();\n    register_events();\n    init_data_tables_and_refreshers();\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/tentacles_configuration.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\nfunction register_and_install_package(){\n    disable_packages_operations();\n    $(\"#register_and_install_package_progess_bar\").show();\n    const element = $(\"#register_and_install_package_input\");\n    const input_text = element.val();\n    const request = {};\n    request[$.trim(input_text)] = \"register_and_install\";\n    const full_config_root = element.parents(\".\"+config_root_class);\n    const update_url = full_config_root.attr(update_url_attr);\n    send_and_interpret_bot_update(request, update_url, full_config_root, post_package_action_success_callback, post_package_action_error_callback)\n}\n\nfunction disable_packages_operations(should_lock=true){\n    const disabled_attr = 'disabled';\n    $(\"#install_tentacles_packages, #update_tentacles_packages, #install-beta-tentacles, #install-regular-tentacles\").prop(disabled_attr, should_lock);\n    $(\"#reset_tentacles_packages\").prop(disabled_attr, should_lock);\n    const register_and_install_package_input = $(\"#register_and_install_package_input\");\n    register_and_install_package_input.prop(disabled_attr, should_lock);\n    if(register_and_install_package_input.val() !== \"\"){\n        $(\"#register_and_install_package_button\").prop(disabled_attr, should_lock);\n    }\n    const should_disable_buttons = get_selected_modules() <= 0;\n    $('#uninstall_selected_tentacles').prop(disabled_attr, should_disable_buttons);\n    $('#update_selected_tentacles').prop(disabled_attr, should_disable_buttons);\n\n}\n\nfunction update(module){\n    perform_modules_operation([module], \"update\");\n}\n\nfunction uninstall(module){\n    if(confirm(\"Uninstall this tentacle ? This will delete the associated tentacle file if any.\")) {\n        perform_modules_operation([module], \"uninstall\");\n    }\n}\n\nfunction perform_modules_operation(modules, operation){\n    const dom_root_element = $(\"#module-table\");\n    const update_url = dom_root_element.attr(operation+\"-\"+update_url_attr);\n    disable_packages_operations();\n    send_and_interpret_bot_update(modules, update_url, dom_root_element, modules_operation_success_callback, modules_operation_error_callback)\n}\n\nfunction perform_packages_operation(source){\n    $(\"#packages_action_progess_bar\").show();\n    const update_url = source.attr(update_url_attr);\n    disable_packages_operations();\n    send_and_interpret_bot_update({}, update_url, source, packages_operation_success_callback, packages_operation_error_callback)\n}\n\nfunction modules_operation_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    disable_packages_operations(false);\n    $(\"#table-span\").load(location.href + \" #table-span\",function(){\n        disable_select_action_buttons();\n        $('#tentacles_modules_table').DataTable({\n            \"paging\":   false,\n        });\n    });\n    $(\"#selected_tentacles_operation\").hide();\n    create_alert(\"success\", \"Tentacle operation success\", msg);\n}\n\nfunction modules_operation_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n    disable_packages_operations(false);\n    $(\"#table-span\").load(location.href + \" #table-span\",function(){\n        disable_select_action_buttons();\n        $('#tentacles_modules_table').DataTable({\n            \"paging\":   false,\n        });\n    });\n    $(\"#selected_tentacles_operation\").hide();\n    create_alert(\"error\", \"Error when managing modules: \"+result.responseText, \"\");\n}\n\nfunction packages_operation_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    disable_packages_operations(false);\n    $(\"#tentacles_modules_table\").load(location.href + \" #tentacles_modules_table\",function(){\n        disable_select_action_buttons();\n    });\n    $(\"#packages_action_progess_bar\").hide();\n    create_alert(\"success\", \"Packages operation success\", msg);\n}\n\nfunction packages_operation_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n    disable_packages_operations(false);\n    $(\"#tentacles_modules_table\").load(location.href + \" #tentacles_modules_table\",function(){\n        disable_select_action_buttons();\n    });\n    $(\"#packages_action_progess_bar\").hide();\n    create_alert(\"error\", \"Error when managing packages: \"+result.responseText, \"\");\n}\n\nfunction post_package_action_success_callback(updated_data, update_url, dom_root_element, msg, status){\n    let package_path;\n    for(const attribute in updated_data) {\n        package_path = attribute;\n    }\n    create_alert(\"success\", \"Tentacles successfully installed\" , \"Packages installed from: \"+package_path);\n    $(\"#tentacles_packages_table\").load(location.href + \" #tentacles_packages_table\");\n    $(\"#tentacles_modules_table\").load(location.href + \" #tentacles_modules_table\",function(){\n        disable_select_action_buttons();\n    });\n    $(\"#register_and_install_package_progess_bar\").hide();\n    disable_packages_operations(false);\n}\n\nfunction post_package_action_error_callback(updated_data, update_url, dom_root_element, result, status, error){\n    create_alert(\"error\", \"Error during package handling: \"+result.responseText, \"\");\n    $(\"#tentacles_packages_table\").load(location.href + \" #tentacles_packages_table\");\n    $(\"#tentacles_modules_table\").load(location.href + \" #tentacles_modules_table\",function(){\n        disable_select_action_buttons();\n    });\n    $(\"#register_and_install_package_progess_bar\").hide();\n    disable_packages_operations(false);\n}\n\nfunction get_selected_modules(){\n    const selected_modules = [];\n    $(\"#module-table\").find(\"input[type='checkbox']:checked\").each(function(){\n        selected_modules.push($(this).attr(\"module\"));\n    });\n    return selected_modules\n}\n\nfunction handle_tentacles_buttons(){\n    $(\"#install_tentacles_packages, #update_tentacles_packages, #install-beta-tentacles, #install-regular-tentacles\").click(function(){\n        perform_packages_operation($(this));\n    });\n    $(\"#reset_tentacles_packages\").click(function(){\n        if(confirm(\"Reset all installed tentacles ? \" +\n            \"WARNING: you will have to re-install the default tentacles and restart your OctoBot to continue \" +\n            \"using this interface (this interface is an OctoBot tentacle). \" +\n            \"This will delete all tentacle files but will save your tentacles configuration.\")) {\n            perform_packages_operation($(this));\n        }\n    });\n    $(\"#uninstall_selected_tentacles\").click(function(){\n        const selected_modules = get_selected_modules();\n        if(selected_modules.length > 0){\n            if(confirm(\"Uninstall these tentacles ? This will delete all the associated tentacle files if any.\")) {\n                $(\"#selected_tentacles_operation\").show();\n                disable_packages_operations();\n                perform_modules_operation(selected_modules,\"uninstall\");\n            }\n        }\n    });\n    $(\"#update_selected_tentacles\").click(function(){\n        const selected_modules = get_selected_modules();\n        if(selected_modules.length > 0){\n            $(\"#selected_tentacles_operation\").show();\n            disable_packages_operations();\n            perform_modules_operation(selected_modules,\"update\");\n        }\n    });\n}\n\nfunction disable_select_action_buttons(){\n    $('#update_selected_tentacles').prop('disabled', true);\n    $('#uninstall_selected_tentacles').prop('disabled', true);\n    $('.selectable_tentacle').click(function () {\n        // use parent not to trigger selection on button column use\n        const row = $(this).parent();\n        if (row.hasClass(selected_item_class)){\n            row.removeClass(selected_item_class);\n            row.find(\".tentacle-module-checkbox\").prop('checked', false);\n        }else{\n            row.toggleClass(selected_item_class);\n            row.find(\".tentacle-module-checkbox\").prop('checked', true);\n        }\n        const should_disable_buttons = get_selected_modules() <= 0;\n        $('#uninstall_selected_tentacles').prop('disabled', should_disable_buttons);\n        $('#update_selected_tentacles').prop('disabled', should_disable_buttons);\n    });\n}\n\n$(document).ready(function() {\n    handle_tentacles_buttons();\n    $('#register_and_install_package_button').prop('disabled', true);\n    $('#register_and_install_package_input').keyup(function() {\n    $('#register_and_install_package_button').prop('disabled', $(this).val() === '');\n    });\n    disable_select_action_buttons();\n    $('#tentacles_modules_table').DataTable({\n        \"paging\":   false,\n    });\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/trading.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\n$(document).ready(async () => {\nconst addOrRemoveWatchedSymbol = (event) => {\n    const sourceElement = $(event.target);\n    const symbol = sourceElement.attr(\"symbol\");\n    let action = \"add\";\n    if(sourceElement.hasClass(\"fas\")){\n        action = \"remove\";\n    }\n    const request = {};\n    request[\"action\"]=action;\n    request[\"symbol\"]=symbol;\n    const update_url = sourceElement.attr(\"update_url\");\n    send_and_interpret_bot_update(request, update_url, sourceElement, watched_symbols_success_callback, watched_symbols_error_callback)\n}\n\nconst watched_symbols_success_callback = (updated_data, update_url, dom_root_element, msg, status) => {\n    create_alert(\"success\", msg, \"\");\n    if(updated_data[\"action\"] === \"add\"){\n        dom_root_element.removeClass(\"far\");\n        dom_root_element.addClass(\"fas\");\n    }else{\n        dom_root_element.removeClass(\"fas\");\n        dom_root_element.addClass(\"far\");\n    }\n}\n\nconst watched_symbols_error_callback = (updated_data, update_url, dom_root_element, result, status, error) => {\n    create_alert(\"error\", result.responseText, \"\");\n}\n\nconst update_pairs_colors = () => {\n    $(\".pair_status_card\").each((_, jselement) => {\n        const element = $(jselement);\n        const first_eval = element.find(\".status\");\n        const status = first_eval.attr(\"status\");\n        if(status.toLowerCase().includes(\"very long\")){\n            element.addClass(\"card-very-long\");\n        }else if(status.toLowerCase().includes(\"long\")){\n            element.addClass(\"card-long\");\n        }else if(status.toLowerCase().includes(\"very short\")){\n            element.addClass(\"card-very-short\");\n        }else if(status.toLowerCase().includes(\"short\")){\n            element.addClass(\"card-short\");\n        }\n    })\n}\n\nconst get_displayed_orders_desc = () => {\n    const orderDescs = [];\n    const cancelButtonIndex = 8;\n    $(\"#orders-table\").DataTable().rows({filter: 'applied'}).data().map((value) => {\n        orderDescs.push(value[cancelButtonIndex]);\n    });\n    return orderDescs;\n}\n\nconst handleClearButton = () => {\n    $(\"#clear-trades-history-button\").on(\"click\", (event) => {\n        if (confirm(\"Clear trades history ?\") === false) {\n            return false;\n        }\n        const url = $(event.currentTarget).data(\"url\")\n        const success = (updated_data, update_url, dom_root_element, msg, status) => {\n            // reload page on success\n            reload_trades(true);\n            reload_pnl(true);\n        }\n        send_and_interpret_bot_update(null, url, null, success, generic_request_failure_callback)\n    })\n}\n\nconst handle_cancel_buttons = () => {\n    $(\"#cancel_all_orders\").click((e) => {\n        const to_cancel_orders = get_displayed_orders_desc();\n        $(\"#ordersCount\").text(to_cancel_orders.length);\n        cancel_after_confirm($('#CancelAllOrdersModal'), to_cancel_orders, $(e.currentTarget).attr(update_url_attr), true);\n    });\n}\n\nconst handle_close_buttons = () => {\n    $(\"button[data-action=close_position]\").each((_, jsElement) => {\n        $(jsElement).click((e) => {\n            const element = $(e.currentTarget);\n            close_after_confirm($('#ClosePositionModal'), element.data(\"position_symbol\"),\n                element.data(\"position_side\"), element.data(\"update-url\"));\n        });\n    });\n}\n\nconst cancel_after_confirm = (modalElement, data, update_url, disable_cancel_buttons=false) => {\n    modalElement.modal(\"toggle\");\n    const confirmButton = modalElement.find(\".btn-danger\");\n    confirmButton.off(\"click\");\n    modalElement.keypress((e) => {\n        if(e.which === 13) {\n            handle_confirm(modalElement, confirmButton, data, update_url, disable_cancel_buttons);\n        }\n    });\n    confirmButton.click(() => {\n        handle_confirm(modalElement, confirmButton, data, update_url, disable_cancel_buttons);\n    });\n}\n\nconst close_after_confirm = (modalElement, symbol, side, update_url) => {\n    modalElement.modal(\"toggle\");\n    const confirmButton = modalElement.find(\".btn-danger\");\n    confirmButton.off(\"click\");\n    const data = {\n        symbol: symbol,\n        side: side,\n    }\n    modalElement.keypress((e) => {\n        if(e.which === 13) {\n            handle_close_confirm(modalElement, confirmButton, data, update_url);\n        }\n    });\n    confirmButton.click(() => {\n        handle_close_confirm(modalElement, confirmButton, data, update_url);\n    });\n}\n\nconst handle_close_confirm = (modalElement, confirmButton, data, update_url) => {\n    send_and_interpret_bot_update(data, update_url, null, orders_request_success_callback, position_request_failure_callback);\n    modalElement.unbind(\"keypress\");\n    modalElement.modal(\"hide\");\n}\n\nconst handle_confirm = (modalElement, confirmButton, data, update_url, disable_cancel_buttons) => {\n    if (disable_cancel_buttons){\n        disable_cancel_all_buttons();\n    }\n    send_and_interpret_bot_update(data, update_url, null, orders_request_success_callback, orders_request_failure_callback);\n    modalElement.unbind(\"keypress\");\n    modalElement.modal(\"hide\");\n}\n\nconst add_cancel_individual_orders_buttons = () => {\n    $(\"button[action=cancel_order]\").each((_, element) => {\n        $(element).off(\"click\");\n        $(element).on(\"click\", (event) => {\n            cancel_after_confirm($('#CancelOrderModal'), $(event.currentTarget).attr(\"order_desc\"), $(event.currentTarget).attr(update_url_attr));\n        });\n    });\n}\n\nconst disable_cancel_all_buttons = () => {\n    $(\"#cancel_all_orders\").prop(\"disabled\",true);\n    $(\"#cancel_order_progress_bar\").show();\n    const cancelIcon = $(\"#cancel_all_icon\");\n    cancelIcon.removeClass(\"fas fa-ban\");\n    cancelIcon.addClass(\"fa fa-spinner fa-spin\");\n    $(\"button[action=cancel_order]\").each((_, jsElement) => {\n        $(jsElement).prop(\"disabled\",true);\n    });\n}\n\nconst orders_request_success_callback = (updated_data, update_url, dom_root_element, msg, status) => {\n    if(msg.hasOwnProperty(\"title\")){\n        create_alert(\"success\", msg[\"title\"], msg[\"details\"]);\n    }else{\n        create_alert(\"success\", msg, \"\");\n    }\n    debouncedReloadDisplay();\n}\n\nconst orders_request_failure_callback = (updated_data, update_url, dom_root_element, msg, status) => {\n    create_alert(\"error\", msg.responseText, \"\");\n    debouncedReloadDisplay();\n}\n\nconst position_request_failure_callback = (updated_data, update_url, dom_root_element, msg, status) => {\n    create_alert(\"error\", msg.responseText, \"\");\n}\n\nconst async_get_data_from_url = async (element) => {\n    const url = element.data(\"url\");\n    if(typeof url === \"undefined\"){\n        return [];\n    }\n    return await async_send_and_interpret_bot_update(null, url, null, \"GET\", true)\n}\n\nconst reload_positions = async (update) => {\n    const table = $(\"#positions-table\");\n    const closePositionUrl = table.data(\"close-url\");\n    const positions = await async_get_data_from_url(table)\n    $(\"#positions-waiter\").hide();\n    displayPositionsTable(\"positions-table\", positions, closePositionUrl, update);\n}\n\nconst reload_trades = async (update) => {\n    const table = $(\"#trades-table\");\n    const refMarket = table.data(\"reference-market\");\n    const trades = await async_get_data_from_url(table)\n    $(\"#trades-waiter\").hide();\n    return displayTradesTable(\"trades-table\", trades, refMarket, update);\n}\n\nconst reload_orders = async (update) => {\n    const table = $(\"#orders-table\");\n    const cancelOrderUrl = table.data(\"cancel-url\");\n    const orders = await async_get_data_from_url(table)\n    $(\"#orders-waiter\").hide();\n    displayOrdersTable(\"orders-table\", orders, cancelOrderUrl, update);\n}\n\nconst registerScaleSelector = () => {\n    $('a[data-action=\"change-scale\"]').on(\"click\", (event) => {\n        const selector = $(event.currentTarget);\n        if(!selector.hasClass(\"active\")){\n            selector.addClass(\"active\");\n            $('a[data-action=\"change-scale\"]').each((_, jselement) => {\n                const element = $(jselement);\n                if(element.data(\"scale\") !== selector.data(\"scale\")){\n                    element.removeClass(\"active\");\n                }\n            })\n            reload_pnl(true);\n        }\n    })\n}\nconst registerSymbolSelector = () => {\n    $(\"#symbol-select\").on(\"change\", () => {\n        reload_pnl(true);\n    })\n}\nconst registerFilterSelectors = () => {\n    registerScaleSelector();\n    registerSymbolSelector();\n}\nconst getScale = () => {\n    return $('a.nav-link.scale-selector.active').data(\"scale\");\n}\nconst getSymbol = () => {\n    return $(\"#symbol-select\").val();\n}\nconst updateTradesCount = (pnlHistory) => {\n    $(\"#match-trades-count\").text(pnlHistory.reduce((sum, element) => sum + element.tc, 0));\n}\nconst reload_pnl = async (update) => {\n    if ($(\"#pnl_historyChart\").length){\n        const pnlHistory = await fetchPnlHistory(getScale(), getSymbol());\n        loadPnlFullChartHistory(pnlHistory, update);\n        loadPnlTableHistory(pnlHistory, update);\n        updateTradesCount(pnlHistory);\n        $(\"#pnl-waiter\").hide();\n    }\n}\n\nconst resizePnlChart = () => {\n    Plotly.Plots.resize(\"pnl_historyChart\")\n}\n\nconst ordersNotificationCallback = (title, _) => {\n    if(title.toLowerCase().indexOf(\"order\") !== -1){\n        debouncedReloadDisplay();\n    }\n}\n\nfunction debouncedReloadDisplay(){\n    debounce(\n        () => reloadDisplay(true),\n        500\n    );\n}\n\nconst reloadDisplay = async (update) => {\n    if(!update){\n        await reload_pnl(update);\n    }\n    await reload_orders(update);\n    await reload_positions(update);\n    if(await reload_trades(update) && update){\n        // only update pnl when a new trade appeared\n        await reload_pnl(update);\n    }\n}\n\nconst onPnlTabShow = (e) => {\n    resizePnlChart();\n}\n\nconst registerOnTabShownEvents = () => {\n    $(\"#panel-pnl-tab\").on(\"shown.bs.tab\", (e) => onPnlTabShow(e));\n}\n\nconst registerTableButtonsEvents = () => {\n    $(\"#positions-table\").on(\"draw.dt\", () => {\n        handle_close_buttons();\n    });\n    $(\"#orders-table\").on(\"draw.dt row-reordered\", () => {\n        if($(\"#orders-table\").data(\"cancel-url\") === undefined){\n            return\n        }\n        add_cancel_individual_orders_buttons();\n        const cancelIcon = $(\"#cancel_all_icon\");\n        $(\"#cancel_order_progress_bar\").hide();\n        if(cancelIcon.hasClass(\"fa-spinner\")){\n            cancelIcon.removeClass(\"fa fa-spinner fa-spin\");\n            cancelIcon.addClass(\"fas fa-ban\");\n        }\n        if ($(\"button[action=cancel_order]\").length === 0){\n            $(\"#cancel_all_orders\").prop(\"disabled\", true);\n        }else{\n            $(\"#cancel_all_orders\").prop(\"disabled\", false);\n        }\n    });\n}\n\nconst refreshTables = async () => {\n    if (!initializedTables){\n        await reloadDisplay(true);\n        initializedTables = true\n    }\n}\n\nlet initializedTables = false\n\nselectFirstTab();\nregisterFilterSelectors();\nregisterTableButtonsEvents();\nupdate_pairs_colors();\n$(\".watched_element\").each((_, element) => {\n    $(element).click(addOrRemoveWatchedSymbol);\n});\nhandle_cancel_buttons();\nhandleClearButton();\nregister_notification_callback(ordersNotificationCallback);\nawait reloadDisplay(false);\nregisterOnTabShownEvents();\nhandle_rounded_numbers_display();\ntry {\n    if(registerGraphUpdateCallback !== undefined){\n        registerGraphUpdateCallback(refreshTables);\n    }\n} catch (error){\n    // nothing to do, registerGraphUpdateCallback doesn't exist\n}\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/trading_type_selector.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    let portfolioEditor = null;\n    let allEditables = [];\n    let updateRequired = false;\n    let initialPortfolioValue = null;\n    let initiallySelectedExchange;\n\n    const getSelectedExchange = () => {\n        return $(\"#AddExchangeSelect\").val();\n    }\n    const displayPortfolioEditor = (currencies) => {\n        const editorDiv = $(\"#portfolio-editor\");\n        let value = editorDiv.data(\"portfolio\");\n        if(initialPortfolioValue === null){\n            initialPortfolioValue = JSON.parse(JSON.stringify(value));  // deep copy initial value\n        }\n        if(typeof value === \"undefined\"){\n            return\n        }\n        const schema = editorDiv.data(\"portfolio-schema\");\n        if(portfolioEditor !== null) {\n            value = portfolioEditor.getValue();\n            portfolioEditor.destroy();\n        }\n        value.forEach((val) => {\n            if(currencies.indexOf(val.asset) === -1){\n                currencies.push(val.asset)\n            }\n        })\n        schema.items.properties.asset.enum = currencies.sort();\n        portfolioEditor = new JSONEditor(editorDiv[0],{\n            schema: schema,\n            startval: value,\n            no_additional_properties: true,\n            prompt_before_delete: false,\n            disable_array_reorder: true,\n            disable_array_delete: false,\n            disable_array_delete_last_row: false,\n            disable_collapse: true,\n            disable_edit_json: true,\n            disable_properties: true,\n        })\n    }\n    const updateCurrencySelector = () => {\n        const editorDiv = $(\"#portfolio-editor\");\n        if(!editorDiv.length){\n            return;\n        }\n        const currencies_url = `${editorDiv.data(\"currencies-url\")}${getSelectedExchange()}`;\n        const successCallback = (updated_data, update_url, dom_root_element, msg, status) => {\n            displayPortfolioEditor(msg)\n        }\n        send_and_interpret_bot_update({}, currencies_url, null, successCallback, generic_request_failure_callback, \"GET\");\n    }\n    const updateSelectedExchange = () => {\n        // update exchange api key form, logo & name\n        const exchangeContent = $(\"#exchanges-tab-content\");\n        const previousExchangeName = exchangeContent.data(\"exchange-name\");\n        if(typeof previousExchangeName === \"undefined\"){\n            log(\"undefined previous exchange name when updating selected exchange\");\n            return\n        }\n        // prevent editable to be stuck open\n        hide_editables(allEditables);\n        const newExchangeName = getSelectedExchange();\n        // update exchange name\n        exchangeContent.data(\"exchange-name\", newExchangeName);\n        exchangeContent.find(`[url=\"/exchange_logo/${previousExchangeName}\"]`).attr(\"src\", \"\").addClass(hidden_class);\n        const toUpdateElements = [\n            $(\"#simulated-config-header\"),\n            $(\"#exchange-container\"),\n        ];\n        toUpdateElements.forEach((element) =>  {\n            element.html(\n                element.html().replace(new RegExp(previousExchangeName,\"g\"), newExchangeName)\n            );\n        })\n        // update logo\n        fetch_images();\n        // trigger accounts check\n        register_exchanges_checks(true);\n        // update form\n        handleEditables();\n    }\n    const registerUpdatesOnExchangeSelect = () => {\n        $(\"#AddExchangeSelect\").on(\"change\", () => {\n            updateCurrencySelector();\n            updateSelectedExchange();\n        })\n    }\n    const getConfigUpdate = (isRealTrading) => {\n        const globalConfigUpdate = {}\n        const removedElements = [];\n        let simulatorEnabled = true;\n        let realEnabled = false;\n        const selectedExchange = getSelectedExchange();\n        const getConfigPortfolioAssetKey = (portfolioAsset) => {\n            return `trader-simulator_starting-portfolio_${portfolioAsset.asset}`;\n        }\n        if(selectedExchange !== initiallySelectedExchange){\n            // update enabled exchange\n            globalConfigUpdate[`exchanges_${initiallySelectedExchange}_enabled`] = false;\n            globalConfigUpdate[`exchanges_${selectedExchange}_enabled`] = true;\n        }\n        if(isRealTrading){\n            const hasValueChanged = (editableElement) => {\n                return editableElement.data(\"changed\") === true;\n            }\n            // update exchange api keys\n            allEditables.forEach((editableElement) => {\n                if(hasValueChanged(editableElement)){\n                    globalConfigUpdate[editableElement.attr(\"config-key\")] = editableElement.text().trim();\n                }\n            })\n            realEnabled = true;\n            simulatorEnabled = false;\n        }else{\n            // update simulated portfolio\n            // trader-simulator_starting-portfolio_BTC\n            const updatedPortfolio = portfolioEditor.getValue();\n            if(initialPortfolioValue !== null &&\n                getValueChangedFromRef(updatedPortfolio, initialPortfolioValue)) {\n                const remainingElements = Array.from(updatedPortfolio, (element) => element.asset);\n                initialPortfolioValue.forEach((portfolioAsset) => {\n                    if(remainingElements.indexOf(portfolioAsset.asset) === -1){\n                        removedElements.push(getConfigPortfolioAssetKey(portfolioAsset))\n                    }\n                });\n                updatedPortfolio.forEach((portfolioAsset) => {\n                    globalConfigUpdate[getConfigPortfolioAssetKey(portfolioAsset)] = portfolioAsset.value;\n                });\n            }\n        }\n        const hasRealTrader = $(\"#exchanges-tab-content\").data(\"has-real-trader\") === \"True\";\n        if((hasRealTrader && !realEnabled) || (!hasRealTrader && !simulatorEnabled)){\n            globalConfigUpdate[\"trader_enabled\"] = realEnabled;\n            globalConfigUpdate[\"trader-simulator_enabled\"] = simulatorEnabled;\n        }\n        updateRequired = Object.keys(globalConfigUpdate).length + removedElements.length > 0;\n        return {\n            global_config: globalConfigUpdate,\n            removed_elements: removedElements,\n        }\n    }\n    const handleStartTradingButtons = () => {\n        $(\"button[data-role=\\\"start-trading\\\"]\").click((e) => {\n            const startButton = $(e.currentTarget);\n            const isRealTrading = startButton.data(\"trading-type\") === \"real\";\n            const configUpdate = getConfigUpdate(isRealTrading);\n            if(!isRealTrading){\n                const errorsDesc = validateJSONEditor(portfolioEditor);\n                if (errorsDesc.length) {\n                    create_alert(\"error\", \"Error in portfolio configuration\", errorsDesc);\n                    return;\n                }\n            }\n            const rebootRequired = updateRequired || new URL(window.location.href).searchParams.get(\"reboot\") === \"True\";\n            const updateUrl = startButton.data(\"config-url\");\n            const startUrl = `${startButton.data(\"start-url\")}${rebootRequired}`;\n            const onSuccess = (updated_data, update_url, dom_root_element, msg, status) => {\n                // redirect to start url\n                window.location.href = startUrl;\n            }\n            if(updateRequired){\n                // update is required before reboot\n                send_and_interpret_bot_update(configUpdate, updateUrl, null,\n                    onSuccess, generic_request_failure_callback);\n            } else {\n                // skip update\n                onSuccess(null, null, null, null, null);\n            }\n        });\n    }\n    const handleEditables = () => {\n        setup_editable();\n        allEditables = handle_editable();\n    }\n\n    initiallySelectedExchange = getSelectedExchange();\n    updateCurrencySelector();\n    displayPortfolioEditor([]);\n    registerUpdatesOnExchangeSelect();\n    handleEditables();\n    handleStartTradingButtons();\n    register_exchanges_checks(true);\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/tradingview_email_config.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n\nconst showVerifCodeError = (errorDetails) => {\n    $(\"[data-role='verification-code-waiter']\").addClass(hidden_class)\n    $(\"[data-role='verification-code-received']\").addClass(hidden_class)\n    $(\"[data-role='verification-code-error']\").removeClass(hidden_class)\n    $(\"#verification-code-error-content\").text(errorDetails)\n}\nconst showVerifCodeWaiter = () => {\n    $(\"[data-role='verification-code-waiter']\").removeClass(hidden_class)\n    $(\"[data-role='verification-code-received']\").addClass(hidden_class)\n    $(\"[data-role='verification-code-error']\").addClass(hidden_class)\n}\nconst showVerifCodeReceived = (confirmEmailContent) => {\n    $(\"[data-role='verification-code-waiter']\").addClass(hidden_class)\n    $(\"[data-role='verification-code-received']\").removeClass(hidden_class)\n    $(\"#verification-code-received-content\").text(confirmEmailContent)\n    $(\"[data-role='verification-code-error']\").addClass(hidden_class)\n}\n\n\nconst triggerEmailConfirmWaiter = async () => {\n    const stepperElement = $(\"#config-stepper\");\n    try {\n        await async_send_and_interpret_bot_update(null, stepperElement.data(\"trigger-verif-code-waiter\"), null, \"POST\", true)\n        let confirmEmailContent = null;\n        const timeout = 5 * 60 * 1000;\n        const t0 = new Date().getTime();\n        while (confirmEmailContent === null) {\n            if (new Date().getTime() - t0 > timeout){\n                showVerifCodeError(\"\");\n            } else {\n                showVerifCodeWaiter();\n            }\n            confirmEmailContent = await async_send_and_interpret_bot_update(null, stepperElement.data(\"get-verif-code-content\"), null, \"GET\", true)\n            await sleep(2000)\n        }\n        if (confirmEmailContent === null) {\n            // error: email was not received within given time\n            showVerifCodeError();\n        }\n        else {\n            showVerifCodeReceived(confirmEmailContent);\n        }\n    } catch(error) {\n        showVerifCodeError(error.responseText)\n    }\n\n}"
  },
  {
    "path": "Services/Interfaces/web_interface/static/js/components/wait_reboot.js",
    "content": "/*\n * Drakkar-Software OctoBot\n * Copyright (c) Drakkar-Software, All rights reserved.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 3.0 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library.\n */\n\n$(document).ready(function() {\n    const onReconnected = () => {\n        const loader = $(\"#restart-loader\");\n        if(loader.length){\n            // change current page when reconnected\n            window.location.href = loader.data(\"redirect-url\");\n        }\n    }\n    registerReconnectedCallback(onReconnected);\n});\n"
  },
  {
    "path": "Services/Interfaces/web_interface/static/license.txt",
    "content": "Material Design for Bootstrap (MDB) under MIT license\n\nCopyright (c) 2017 MDBootstrap.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation \nfiles (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, \nmodify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software \nis furnished to do so, subject to the following conditions: \n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. \n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO \nTHE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR \nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, \nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "Services/Interfaces/web_interface/templates/404.html",
    "content": "{% extends \"layout.html\" %}\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>We are sorry, but this doesn't exist.</h2>\n    </div>\n    <div class=\"card-body\">\n        You may have mistyped the address or the page may have moved.\n    </div>\n    <div class=\"card-footer\">\n        <button type=\"button\" id=\"back-button\" class=\"btn btn-lg btn-primary waves-effect\">Go back</button>\n    </div>\n</div>\n{% endblock %}\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/500.html",
    "content": "{% extends \"layout.html\" %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>We are sorry, but an unexpected error occurred.</h2>\n    </div>\n    <div class=\"card-body\">\n        {{ m_flash_messages.flash_messages() }}\n        <p>\n            Error: {{ error }} ({{ error.__class__.__name__ }}). More details in logs.\n        </p>\n        <p>\n            If you believe this is an issue with OctoBot, please open an issue describing what happened on the\n            <a target=\"_blank\" rel=\"noopener\" href=\"https://github.com/Drakkar-Software/OctoBot/issues\"\n               class=\"btn btn-outline-primary btn-md waves-effect\">OctoBot issues report system.</a>\n        </p>\n    </div>\n    <div class=\"card-footer\">\n        <button type=\"button\" id=\"back-button\" class=\"btn btn-lg btn-primary waves-effect\">Go back</button>\n    </div>\n</div>\n{% endblock %}\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/about.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"about\" %}\n{% import 'macros/text.html' as m_text %}\n{% import \"components/community/octobot_cloud_description.html\" as octobot_cloud_description %}\n\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>Your OctoBot</h2>\n    </div>\n    <div class=\"card-body\">\n        <button route=\"{{ url_for('commands', cmd='restart') }}\" type=\"button\" class=\"btn btn-warning waves-effect\">Restart Octobot</button>\n        <button route=\"{{ url_for('commands', cmd='stop') }}\" type=\"button\" class=\"btn btn-danger waves-effect\">Stop Octobot</button>\n        {% if current_user.is_authenticated %}\n            <div class=\"float-right\">\n                <a href=\"{{ url_for('logout') }}\" class=\"btn btn-outline-warning waves-effect\">\n                    <i class=\"fas fa-sign-out-alt\"></i>\n                    Lock\n                </a>\n            </div>\n        {% endif %}\n    </div>\n</div>\n<br>\n<div class=\"card\" id=\"hosting\">\n    <div class=\"card-header\">\n        <h2>Get more from OctoBot using OctoBot cloud</h2>\n    </div>\n    <div class=\"card-body py-0\">\n        {{ octobot_cloud_description.octobot_cloud_description(OCTOBOT_COMMUNITY_URL, LOCALE) }}\n    </div>\n</div>\n<br>\n\n<div class=\"card\" id=\"support\">\n    <div class=\"card-header\">\n        <h2>Running your OctoBot on the cloud</h2>\n    </div>\n    <div class=\"card-body\">\n        <p>\n            While it is possible to use OctoBot directly from your computer as much as you want, you can also\n            also <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-installation/cloud-install-octobot-on-digitalocean?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=about_cloud_hosting\">\n            easily host your OctoBot on the cloud using DigitalOcean</a>.\n        </p>\n        <p>\n            OctoBot is available from on the <a target=\"_blank\" rel=\"noopener\" href=\"https://digitalocean.pxf.io/octobot-app\">DigitalOcean marketplace</a>.\n            It enables you to have your OctoBot executing your trading strategies 24/7 without having to leave a computer on.\n        </p>\n        <div>\n            <a href=\"https://digitalocean.pxf.io/start-octobot\">\n                <img src=\"https://mp-assets1.sfo2.digitaloceanspaces.com/deploy-to-do/do-btn-blue.svg\" alt=\"Deploy on DO\"/>\n            </a>\n        </div>\n    </div>\n</div>\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>Help us to improve OctoBot</h2>\n    </div>\n    <div class=\"card-body\">\n        <div class=\"mb-4\">\n            Any question ?\n            Please have a look at our\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-usage/frequently-asked-questions-faq?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=faq\">\n                Frequently ask question (FAQ) section\n            </a>\n            first !\n        </div>\n        {% if not IS_CLOUD %}\n        <div class=\"custom-control custom-switch\">\n            <input type=\"checkbox\" class=\"custom-control-input\" id=\"metricsCheckbox\" update-url=\"{{ url_for('metrics_settings') }}\" {{ 'checked' if metrics_enabled else ''}}>\n            <label class=\"custom-control-label\" for=\"metricsCheckbox\">Share</label> metrics to help the OctoBot Community\n            <p>\n                This will greatly help the OctoBot team to figure out the best ways to improve OctoBot and won't slow your OctoBot down.\n            </p>\n        </div>\n        {% endif %}\n        <hr>\n        <div>\n            <p>\n                In order to improve OctoBot, your user feedback is extremely helpful. The best way to make this project better and better is by telling us about your experience\n                (positive and negative) when using OctoBot.\n            </p>\n            <a update-url=\"{{ url_for('api.user_feedback') }}\" href=\"\" id=feedbackButton class=\"btn btn-primary waves-effect disabled\" target=\"_blank\" rel=\"noopener\">\n                Tell us what you think about OctoBot\n            </a>\n            <a href=\"{{ OCTOBOT_FEEDBACK_URL }}open-source\" id=suggestButton class=\"btn btn-primary waves-effect\" target=\"_blank\" rel=\"noopener\">\n                Suggest a feature for OctoBot\n            </a>\n        </div>\n    </div>\n</div>\n<br>\n\n<div class=\"card\">\n    <div class=\"card-header\"><h2>Disclaimer</h2></div>\n    <div class=\"card-body\">\n        {{ m_text.text_lines(disclaimer) }}\n        <p>\n            <a href=\"{{ url_for('terms') }}\">\n                Terms and conditions\n            </a>\n        </p>\n    </div>\n</div>\n<br>\n\n<div class=\"card\" id=\"beta-program\">\n    <div class=\"card-header\"><h2>OctoBot Beta Tester program</h2></div>\n    <div class=\"card-body\">\n        <p>\n            You can help the team improving OctoBot by testing features in advance through the beta tester group.\n            Registering to the beta tester group will grant you access to major new features weeks in advance as well\n            as a direct communication channel to the OctoBot team to share your feedback and ideas before new versions are\n            released to the public.\n            <a href=\"{{OCTOBOT_DOCS_URL}}/octobot-advanced-usage/beta-program?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=beta_program\" target=\"_blank\" rel=\"noopener\">\n                More info on the beta tester program\n            </a>\n        </p>\n        <div class=\"custom-control custom-switch my-2\">\n            <input type=\"checkbox\" class=\"custom-control-input\" id=\"beta-checkbox\" update-url=\"{{ url_for('beta_env_settings') }}\"\n                   {{ 'checked' if beta_env_enabled_in_config else ''}}\n            >\n            <label class=\"custom-control-label\" for=\"beta-checkbox\">Connect to the beta environment</label>\n        </div>\n        <div class=\"alert alert-info\">\n            When the beta environment is enabled, you will be connected to the \"in development\" version of {{OCTOBOT_COMMUNITY_URL}}.\n            Available elements will be different from normal ones and your OctoBot might produce unexpected behaviors.\n            Only enable it when actively beta testing and disable it afterwards.\n        </div>\n\n        <p>\n            <a href=\"{{ octobot_beta_program_form_url }}\" class=\"btn btn-primary waves-effect\" target=\"_blank\" rel=\"noopener\">\n                Register to the beta tester program\n            </a>\n        </p>\n    </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/commands.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/accounts.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"accounts\" %}\n{% set startup_messages_added_classes = \"justify-content-end px-4\" %}\n{% set inner_startup_messages_added_classes = \"offset-md-3 offset-lg-2 offset-1\" %}\n{% import 'components/config/exchange_card.html' as m_config_exchange_card %}\n{% import 'components/config/service_card.html' as m_config_service_card %}\n{% import 'components/config/notification_config.html' as m_config_notification %}\n\n{% set config_default_value = \"Bitcoin\" %}\n{% set added_class = \"new_element\" %}\n\n\n{% block additional_style %}\n    <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/components/configuration.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n{% endblock additional_style %}\n\n{% block body %}\n<div class=\"row mt-md-4 mt-2\">\n  <nav class=\"mt-md-4 mt-2 col-md-3 col-lg-2 col-1 d-block sidebar shadow\">\n      <div class=\"sidebar-sticky mt-0 pt-0\">\n        <div class=\"col-8 d-none d-md-block\">\n            <div class=\"px-1 px-md-4\">\n                <h4>Accounts settings</h4>\n            </div>\n        </div>\n        <div class=\"nav flex-column bordered pt-0 mt-0 mt-md-4\" id=\"v-tab\" role=\"tablist\" aria-orientation=\"vertical\">\n          <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" data-tab=\"default\" id=\"panelExchanges-tab\" data-toggle=\"pill\" href=\"#panelExchanges\" role=\"tab\" aria-controls=\"panelExchanges\" aria-selected=\"false\">\n              <i class=\"fas fa-exchange-alt my-auto\"></i><span class=\"d-none d-md-block pl-3\">Exchanges</span>\n          </a>\n          <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelServices-tab\" data-toggle=\"pill\" href=\"#panelServices\" role=\"tab\" aria-controls=\"panelServices\" aria-selected=\"false\">\n              <i class=\"fas fa-share-alt my-auto\"></i><span class=\"d-none d-md-block pl-3\">Interfaces</span>\n          </a>\n          <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelNotifications-tab\" data-toggle=\"pill\" href=\"#panelNotifications\" role=\"tab\" aria-controls=\"panelNotifications\" aria-selected=\"false\">\n              <i class=\"fas fa-bell my-auto\"></i><span class=\"d-none d-md-block pl-3\">Notifications</span>\n          </a>\n        </div>\n        <a class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0\" id=\"save-config\" href=\"#\" role=\"tab\" aria-selected=\"false\" update-url=\"{{ url_for('config') }}\">\n            <i class=\"fas fa-save my-auto\"></i><span class=\"d-none d-md-block pl-2\">Save</span>\n        </a>\n        <a class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0\" id=\"reset-config\" href=\"#\" role=\"tab\" aria-selected=\"false\">\n            <i class=\"fas fa-redo-alt my-auto\"></i><span class=\"d-none d-md-block pl-2\">Reset all</span>\n        </a>\n        <button class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0 mt-3 mt-mb-5 mb-5\" id=\"save-config-and-restart\" href=\"#\" role=\"tab\" type=\"button\" aria-selected=\"false\" update-url=\"{{ url_for('config') }}\">\n            <i class=\"fas fa-power-off my-auto\"></i><span class=\"d-none d-md-block pl-2\">Apply changes and restart</span>\n        </button>\n    </div>\n  </nav>\n  <main role=\"main\" class=\"col-md-9 col-lg-10 col-11 ml-auto px-4\">\n    <div class=\"tab-content\" id=\"super-container\">\n      <div class=\"tab-pane fade config-root show\" id=\"panelExchanges\" role=\"tabpanel\" aria-labelledby=\"panelExchanges-tab\">\n          <div class=\"card\">\n            <div class=\"card-header\">\n                <h2>Exchanges\n                    <a class=\"float-right blue-text\" target=\"_blank\" data-intro=\"account_exchanges\">\n                        <i class=\"fa-solid fa-question\"></i>\n                    </a>\n                </h2>\n            </div>\n            <div class=\"card-body deck-container\">\n                <div class=\"card\" id=\"new-exchange-selector\">\n                    <div class=\"card-body\">Add a new exchange :\n                        <select id=\"AddExchangeSelect\" class=\"selectpicker\" data-live-search=\"true\" data-window-padding=\"50\">\n                            <optgroup label=\"OctoBot fully tested\">\n                               {% for exchange in ccxt_tested_exchanges %}\n                                    {% set has_ws = exchanges_details[exchange] and exchanges_details[exchange]['has_websockets'] %}\n                                    <option data-tokens=\"{{ exchange }}\" data-ws=\"{{ 'True' if has_ws else 'False'}}\">{{ exchange }}</option>\n                               {% endfor %}\n                            </optgroup>\n                            {% if ccxt_simulated_tested_exchanges %}\n                            <optgroup label=\"OctoBot tested with simulated trading\">\n                               {% for exchange in ccxt_simulated_tested_exchanges %}\n                                    {% set has_ws = exchanges_details[exchange] and exchanges_details[exchange]['has_websockets'] %}\n                                    <option data-tokens=\"{{ exchange }}\" data-ws=\"{{ 'True' if has_ws else 'False' }}\">{{ exchange }}</option>\n                               {% endfor %}\n                            </optgroup>\n                            {% endif %}\n                            <optgroup label=\"OctoBot untested\">\n                               {% for exchange in ccxt_other_exchanges %}\n                                    {% set has_ws = exchanges_details[exchange] and exchanges_details[exchange]['has_websockets'] %}\n                                    <option data-tokens=\"{{ exchange }}\" data-ws=\"{{ 'True' if has_ws else 'False' }}\">{{ exchange }}</option>\n                               {% endfor %}\n                            </optgroup>\n                        </select>\n                        <button type=\"button\" id=\"AddExchange\" class=\"btn btn-primary add-btn px-3 waves-effect\"><i class=\"fa fa-plus pr-2\" aria-hidden=\"true\"></i> Add</button>\n                    </div>\n                </div>\n                <br>\n                <div class=\"alert alert-info\" role=\"alert\">\n                    <i class=\"fa-regular fa-lightbulb\"></i> Add exchanges here and select the ones to use in your profile settings. Switch to <span class=\"font-weight-bold\">futures or spot</span> trading in your <a href=\"{{url_for('profile', _anchor='panelExchanges')}}\">profile exchange settings</a>.\n                </div>\n                <!-- Card deck -->\n                <div class=\"card-deck config-container\" id=\"exchange-container\" update-url=\"{{ url_for('api.are_compatible_accounts') }}\">\n                    {% for exchange in config_exchanges %}\n                        {{ m_config_exchange_card.config_exchange_card(config_exchanges,\n                                                                       exchange,\n                                                                       exchanges_details[exchange],\n                                                                       is_supporting_future_trading,\n                                                                       enabled=config_exchanges[exchange].get('enabled', True),\n                                                                       sandboxed=config_exchanges[exchange].get('sandboxed', False),\n                                                                       selected_exchange_type=config_exchanges[exchange].get('exchange-type', 'spot'),\n                                                                       full_config=True)}}\n                    {% endfor %}\n                </div>\n            </div>\n        </div>\n      </div>\n      <div class=\"tab-pane fade config-root\" id=\"panelServices\" role=\"tabpanel\" aria-labelledby=\"panelServices-tab\">\n          <div class=\"card\">\n            <div class=\"card-header\"><h2>Interfaces</h2></div>\n            <div class=\"card-body deck-container\">\n                <div class=\"card\">\n                    <div class=\"card-body\">Add a new interface :\n                    <select id=\"AddServiceSelect\" class=\"selectpicker\" data-live-search=\"true\">\n                       {% for service in services_list | sort() %}\n                            <option data-tokens=\"{{ service }}\">{{ service }}</option>\n                       {% endfor %}\n                    </select>\n                    <button type=\"button\" id=\"AddService\" class=\"btn btn-primary add-btn px-3 waves-effect\"><i class=\"fa fa-plus pr-2\" aria-hidden=\"true\"></i> Add</button>\n                    </div>\n                </div>\n                <br>\n                <!-- Card deck -->\n                <div class=\"card-deck config-container\" update-url=\"{{ url_for('config') }}\">\n                    {% for service in services_list %}\n                        {% if service in config_services %}\n                            {{ m_config_service_card.config_service_card(config_services, service, services_list[service], extension_name=OCTOBOT_EXTENSION_PACKAGE_1_NAME) }}\n                        {% endif %}\n                    {% endfor %}\n                </div>\n            </div>\n        </div>\n      </div>\n      <div class=\"tab-pane fade config-root\" id=\"panelNotifications\" role=\"tabpanel\" aria-labelledby=\"panelNotifications-tab\">\n          <div class=\"card\">\n            <div class=\"card-header\">\n                <h2>\n                    Notifications\n                    <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-configuration/accounts#notifications\">\n                        <i class=\"fa-solid fa-question\"></i>\n                    </a>\n                </h2>\n            </div>\n            <div class=\"card-body deck-container\">\n                <!-- Card deck -->\n                <div class=\"card-deck config-container\">\n                    {{ m_config_notification.config_notification(config_notifications, \"notification\", notifiers_list) }}\n                </div>\n            </div>\n        </div>\n      </div>\n    </div>\n  </main>\n</div>\n\n<!-- Default cards -->\n<div class=\"d-none\">\n    <!-- Exchange -->\n    <div id=\"AddExchange-template-default\">\n        {{ m_config_exchange_card.config_exchange_card( config=config_exchanges,\n                                                        exchange=config_default_value,\n                                                        exchanges_details=exchanges_details,\n                                                        is_supporting_future_trading=is_supporting_future_trading,\n                                                        add_class=added_class,\n                                                        keys_value=\"NO KEY\",\n                                                        config_values=\"no value\",\n                                                        full_config=True) }}\n    </div>\n\n    <!-- Services -->\n    <div id=\"AddService-template-default\">\n        {% for service in services_list %}\n            <div id=\"AddService-template-default-{{service}}\">\n            {{ m_config_service_card.config_service_card(  config_services,\n                                                           service,\n                                                           services_list[service],\n                                                           add_class=added_class,\n                                                           no_select=True,\n                                                           default_values=True,\n                                                           extension_name=OCTOBOT_EXTENSION_PACKAGE_1_NAME) }}\n            </div>\n        {% endfor %}\n    </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/exchange_accounts.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/automations.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n\n{% macro automation_details(automations) %}\n    <ul>\n    {% for automation in automations %}\n        <li>\n            <span class=\"font-weight-bold\">{{automation.get_name()}}</span>: {{automation.get_description() }}\n        </li>\n    {% endfor %}\n    </ul>\n{% endmacro %}\n\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2 id=\"page-title\">Automations\n            <span class=\"d-none d-md-inline\">\n                configuration of the <span class=\"font-weight-bold\">{{ profile_name }}</span> profile\n                <a class=\"btn btn-sm rounded-circle btn-primary waves-effect mx-1 mx-md-4 align-top\"\n                   href=\"{{url_for('profiles_selector')}}\" data-toggle=\"tooltip\"\n                   title=\"Select another profile\"\n                   id=\"profile-selector-link\"\n                >\n                    <i class=\"fas fa-exchange-alt\" aria-hidden=\"true\"></i>\n                </a>\n            </span>\n            <a class=\"float-right blue-text\" target=\"_blank\" data-intro=\"automations\" >\n                <i class=\"fa-solid fa-question\"></i>\n            </a>\n        </h2>\n    </div>\n    <div class=\"card-body\" id=\"configEditorBody\"\n         data-edit-details-url=\"{{ url_for('automations_edit_details') }}\">\n        <div class=\"row\">\n            <div class=\"col-12 col-md-10\">\n                <p>\n                    Automations are actions that will be triggered automatically when something happens.\n                    You can have as many automations as you want. Automation are started automatically\n                    when your OctoBot starts and when hitting <i class=\"fas fa-save\"></i> Apply.\n                </p>\n            </div>\n            <div class=\"col col-md-2\">\n                <button class=\"btn btn-outline-primary btn-md waves-effect\"\n                        data-toggle=\"modal\" data-target=\"#AutomationDetailsModal\">\n                    Automation details\n                </button>\n            </div>\n        </div>\n        <div id=\"configEditor\"></div>\n        <div id=\"configEditorButtons\" class=\"d-none\">\n            <div class=\"text-center\">\n                <button class=\"btn btn-primary waves-effect\" data-role='add-automation'\n                ><i class=\"fas fa-plus\"></i> <span class=\"d-none d-md-inline-block\">Add automation</span></button>\n                <button class=\"btn btn-outline-primary waves-effect\" data-role='remove-automation'\n                ><i class=\"fas fa-minus\"></i> <span class=\"d-none d-md-inline-block\">Remove last automation</span></button>\n            </div>\n        </div>\n        <div id=\"editor-waiter\" class=\"text-center my-4\">\n            <div>\n                <h2>Loading configuration</h2>\n            </div>\n            <div>\n                <h2><i class=\"fa fa-spinner fa-spin\"></i></h2>\n            </div>\n        </div>\n        <div id=\"configErrorDetails\" style='display: none;'>\n            <div>\n                Error when fetching automation config. Resetting its configuration should fix the issue.\n            </div>\n            <div>\n                <button class=\"btn btn-warning waves-effect\" data-role='factoryResetConfig'\n                        update-url=\"{{ url_for('automations', action='factory_reset') }}\"><i class=\"fas fa-recycle\"></i> Reset configuration to default values</button>\n            </div>\n        </div>\n    </div>\n    <div class=\"card-footer\" id='saveConfigFooter' style='display: none;'>\n        <button id=\"applyAutomations\" class=\"btn btn-primary waves-effect\" data-role='startAutomations' update-url=\"{{ url_for('automations', action='start') }}\">\n            <i class=\"fas fa-save\"></i> Apply\n        </button>\n        <button class=\"btn btn-outline-warning waves-effect float-right\" data-role='factoryResetConfig'\n                update-url=\"{{ url_for('config_tentacle', name=name, action='factory_reset') }}\"><i class=\"fas fa-recycle\"></i> Reset all</button>\n    </div>\n</div>\n<span class=\"d-none\">\n    <button class=\"d-none\" data-role='saveConfig' update-url=\"{{ url_for('automations', action='save') }}\">Save</button>\n</span>\n<br>\n\n\n<div class=\"modal fade\" id=\"AutomationDetailsModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#AutomationDetailsModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog modal-dialog-centered modal-xl\" role=\"document\">\n    <div class=\"modal-content modal-text mt-4\">\n      <div class=\"modal-header primary-text\">\n          <h5 class=\"modal-title\" id=\"AutomationDetailsModalLabel\">\n              Available automations\n          </h5>\n        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n      </div>\n      <div class=\"modal-body\">\n          <div>\n              <h5>Triggers</h5>\n              <p>\n                  When your automation should be executed.\n              </p>\n              {{ automation_details(events.values()) }}\n          </div>\n          <div>\n              <h5>Conditions</h5>\n              <p>\n                  Checks to perform when a trigger occurs to know if actions should be executed.\n              </p>\n              {{ automation_details(conditions.values()) }}\n          </div>\n          <div>\n              <h5>Actions</h5>\n              <p>\n                  What to do when triggered and passing conditions.\n              </p>\n              {{ automation_details(actions.values()) }}\n          </div>\n      </div>\n      <div class=\"modal-footer d-flex justify-content-between\">\n        <a class=\"btn btn-outline-primary waves-effect\" href=\"{{url_for('dsl_help')}}\" target=\"_blank\" rel=\"noopener\">OctoBot DSL Help</a>\n        <button class=\"btn btn-outline-primary waves-effect\" data-dismiss=\"modal\">Close</button>\n      </div>\n    </div>\n  </div>\n</div>\n\n<span class=\"d-none\" data-display-intro=\"{{display_intro}}\"></span>\n<span class=\"d-none\" id=\"feedback-form-data\"\n      data-display-form=\"{{display_feedback_form}}\"\n      data-user-id=\"{{user_id}}\"\n      data-form-to-display=\"{{form_to_display}}\"\n      data-on-submit-url=\"{{url_for('api.register_submitted_form')}}\"\n></span>\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/config_tentacle.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/automations.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/backtesting.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"backtesting\" %}\n\n{% import 'macros/backtesting_utils.html' as m_backtesting_utils %}\n\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\" id=\"backtestingPage\" update-url=\"{{ url_for('backtesting', update_type='backtesting_status') }}\">\n        <h2>Backtesting\n\n            <a class=\"float-right blue-text\" target=\"_blank\" data-intro=\"backtesting\">\n                <i class=\"fa-solid fa-question\"></i>\n            </a>\n            {% if activated_trading_mode %}\n            <a class=\"float-right badge badge-info waves-effect mx-2\" href=\"{{ url_for('config_tentacle', name=activated_trading_mode.get_name()) }}\">\n                Current trading mode: {{ activated_trading_mode.get_name() }}\n            </a>\n            {% endif %}\n        </h2>\n    </div>\n    <div class=\"card-body\">\n        {% if data_files %}\n            <div class=\"text-center\">\n                            <a href=\"{{ url_for('data_collector') }}\"\n                               id=\"data-collector-link\"\n                               class=\"btn btn-outline-info waves-effect\">\n                                <i class=\"fa fa-cloud-download-alt\"></i> Get historical data\n                            </a>\n\n            </div>\n            <table class=\"table table-striped table-sm table-responsive-xs\" id=\"dataFilesTable\">\n              <caption>Select data file(s) to use</caption>\n              <thead>\n                <tr>\n                    <th scope=\"col\">#</th>\n                    <th scope=\"col\">Symbol(s)</th>\n                    <th scope=\"col\">Date of recording</th>\n                    <th scope=\"col\">Candles</th>\n                    <th scope=\"col\">Exchange</th>\n                    <th scope=\"col\">Time frame(s)</th>\n                    <th scope=\"col\">File</th>\n                </tr>\n              </thead>\n              <tbody>\n                {% for file, description in data_files %}\n                    <tr class=\"selectable_datafile\">\n                        <td><div class=\"custom-control custom-checkbox\">\n                            <input type=\"checkbox\" class=\"custom-control-input dataFileCheckbox\" data-file=\"{{file}}\" symbols=\"{{description.symbols}}\">\n                            <label class=\"custom-control-label\"></label>\n                        </div></td>\n                        <td>{{\", \".join(description.symbols)}}</td>\n                        {% if description.start_timestamp %}\n                            <td data-order=\"{{description.timestamp}}\" data-start-timestamp=\"{{description.start_timestamp}}\"\n                                    data-end-timestamp=\"{{description.end_timestamp}}\">\n                                {{description.start_date}} to {{description.end_date}}\n                            </td>\n                            <td>Full</td>\n                        {% else %}\n                            <td data-order=\"{{description.timestamp}}\">{{description.date}}</td>\n                            <td>{{description.candles_length}}</td>\n                        {% endif %}\n                        <td>{{description.exchange}}</td>\n                        <td>{{\", \".join(description.time_frames)}}</td>\n                        <td>{{file}}</td>\n                    </tr>\n                {% endfor %}\n              </tbody>\n            </table>\n            {% if activated_trading_mode and activated_trading_mode.is_backtestable() %}\n                <div class=\"row mx-0 mt-0\">\n                    <div class=\"row col-12 col-md-3\">\n                        <div class=\"custom-control custom-switch mx-4 d-none my-auto\" id=\"synchronized-data-only-div\">\n                            <input type=\"checkbox\" class=\"custom-control-input\" id=\"synchronized-data-only-checkbox\" checked=\"\">\n                            <label class=\"custom-control-label\" for=\"synchronized-data-only-checkbox\">Run on synchronized history only</label>\n                        </div>\n                    </div>\n                    <div class=\"row\">\n                        <div class=\"offset-md-4 col-12 col-md-2 my-auto\">\n                            Start Date :\n                            <input type=\"date\" class=\"datepicker\" id=\"startDate\">\n                        </div>\n                        <div class=\"col-12 col-md-2 my-auto\">\n                            End Date :\n                            <input type=\"date\" class=\"datepicker\" id=\"endDate\">\n                        </div>\n                    </div>\n                    <div class=\"row justify-content-center mt-4\">\n                        <div class=\"my-auto text-center\">\n                            <button type=\"button\" id=\"startBacktesting\" class=\"btn btn-lg btn-primary waves-effect ml-auto\"\n                                start-url=\"/backtesting?action_type=start_backtesting&amp;source=backtesting\">\n                                <i class=\"fa fa-play\"></i> Start backtesting\n                            </button>\n                        </div>\n                    </div>\n                </div>\n            {% elif activated_trading_mode %}\n                <div class=\"alert alert-warning mt-1 text-center\" role=\"alert\">\n                    <a class=\"alert-link\" href=\"{{ url_for('config_tentacle', name=activated_trading_mode.get_name()) }}\">\n                        {{ activated_trading_mode.get_name() }}</a>\n                    can't be used in backtesting.\n                    <a href=\"{{ url_for('profile')}}\">Select another profile or trading mode</a> to run a backtesting.\n                </div>\n            {% elif not activated_trading_mode %}\n                <div class=\"alert alert-warning mt-1 text-center\" role=\"alert\">\n                    No active trading mode.\n                </div>\n            {% endif %}\n            <span id='backtesting_progress_bar' style='display: none;'>\n                <div class=\"card-title\">\n                    <h2>Backtesting in progress</h2>\n                </div>\n\n                <div class='progress'>\n                  <div id='progess_bar_anim' class='progress-bar progress-bar-striped progress-bar-animated'\n                        role='progressbar' aria-valuenow='0' aria-valuemin='0' aria-valuemax='100' style='width: 0%;'></div>\n                </div>\n            </span>\n        {% else %}\n            <h4 class=\"py-3\">\n                No backtesting data files found. Once you have data files, they will be displayed here to be used in backtesting.\n            </h4>\n            <div class=\"d-flex justify-content-center\">\n                <a href=\"{{ url_for('data_collector') }}\" class=\"btn btn-primary waves-effect\"> <i class=\"fa fa-cloud-download-alt\"></i> Get historical data</a>\n            </div>\n        {% endif %}\n    </div>\n</div>\n<br>\n{{ m_backtesting_utils.backtesting_report('backtesting', OCTOBOT_DOCS_URL, has_open_source_package) }}\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/tables_display.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/candlesticks.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/backtesting_util.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/backtesting.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/community.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"community\" %}\n\n{% import \"components/community/bot_selector.html\" as bot_selector %}\n{% import \"components/community/cloud_strategies.html\" as cloud_strategies_display %}\n{% import \"components/community/bots_stats.html\" as bots_stats %}\n{% import \"components/community/tentacle_packages.html\" as m_tentacle_packages %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n\n{% block body %}\n<br>\n\n{{ bots_stats.bots_stats_card(current_bots_stats) }}\n\n\n{{ m_flash_messages.flash_messages() }}\n\n{{ m_tentacle_packages.pending_tentacles_install_modal(has_owned_packages_to_install and not auto_refresh_packages) }}\n{{ cloud_strategies_display.cloud_strategies(strategies, OCTOBOT_COMMUNITY_URL, LOCALE, OCTOBOT_EXTENSION_PACKAGE_1_NAME, has_open_source_package) }}\n\n<div class=\"card\">\n    <div class=\"card-body\">\n        <div class=\"row py-0\">\n            <div class=\"col-8 my-auto\">\n                <h5 class=\"p-0 my-0\">\n\n                    Logged in as {{current_logged_in_email}}\n                    {% if selected_user_bot[\"name\"] %}\n                        using bot <span class=\"badge badge-info\">{{ selected_user_bot[\"name\"] }}</span>\n                    {% else %}\n                        <span class=\"badge badge-danger\">without selected bot</span>\n                    {% endif %}\n                    {% if can_select_bot %}\n                    <a class=\"btn btn-sm btn-outline-primary waves-effect\" href=\"#\" id=\"display-bot-select-modal\"\n                       data-toggle=\"modal\" data-target=\"#bot-select-modal\">\n                       Select bot\n                    </a>\n                    {% endif %}\n                    <span class=\"ml-2 ml-md-4 text-danger-darker\">\n                        {% if 'tester' in role %}\n                            <i class=\"fas fa-tools\" data-toggle=\"tooltip\" title=\"OctoBot tester\"></i>\n                        {% elif 'contributor' in role %}\n                            <i class=\"fas fa-laptop-code\" data-toggle=\"tooltip\" title=\"OctoBot contributor\"></i>\n                        {% elif 'sponsor' in role %}\n                            <i class=\"fas fa-star\" data-toggle=\"tooltip\" title=\"OctoBot sponsor\"></i>\n                        {% endif %}\n                        {% if is_donor %}\n                            <i class=\"fas fa-trophy\" data-toggle=\"tooltip\"\n                               title=\"OctoBot donor: On behalf of the OctoBot team, thank you for being awesome and your donating to the project.\"></i>\n                        {% endif %}\n                    </span>\n                </h5>\n            </div>\n            {% if can_logout %}\n                <div class=\"col-4 text-right\">\n                    <a class=\"align-right btn btn-sm btn-outline-info waves-effect\"\n                       href=\"{{ url_for('community_logout')}}\">\n                        logout\n                    </a>\n                </div>\n            {% endif %}\n        </div>\n        {% if can_select_bot %}\n        <div class=\"modal\" id=\"bot-select-modal\" tabindex=\"-1\" role=\"dialog\"\n             aria-labelledby=\"#display-bot-select-modal\" aria-hidden=\"true\">\n          <div class=\"modal-dialog modal-dialog-centered modal-lg\" role=\"document\">\n            <div class=\"modal-content modal-text\">\n              <div class=\"modal-header primary-text\">\n                <h2 class=\"modal-title\">Bot selection</h2>\n                    {% if can_logout and not selected_user_bot[\"id\"]%}\n                    <div class=\"text-right\">\n                        <a class=\"btn btn-sm btn-outline-info waves-effect\"\n                           href=\"{{ url_for('community_logout')}}\">\n                            logout\n                        </a>\n                    </div>\n                    {% endif %}\n                  {% if selected_user_bot[\"id\"] %}\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n                      <span aria-hidden=\"true\">&times;</span>\n                    </button>\n                  {% endif %}\n              </div>\n              <div class=\"modal-body\">\n                {{ bot_selector.bot_selector(all_user_bots, selected_user_bot) }}\n              </div>\n            </div>\n          </div>\n        </div>\n        {% endif %}\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/extensions.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/community.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/community_login.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"community\" %}\n\n{% import \"components/community/bots_stats.html\" as bots_stats %}\n{% import \"components/community/login.html\" as login %}\n\n{% block body %}\n<br>\n{{ bots_stats.bots_stats_card(current_bots_stats) }}\n\n<div class=\"login_box mx-auto mt-1 mt-xl-5\">\n    <div class=\"card\">\n        {{ login.login_form(form, is_in_stating_community_env, next_url, OCTOBOT_COMMUNITY_RECOVER_PASSWORD_URL) }}\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/community.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/community_metrics.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"community\" %}\n{% import 'macros/tables.html' as m_tables %}\n\n{% macro top_table_card(items, item_name, table_name) -%}\n    <div class=\"col-12 col-md-5 card block-card d-block mb-4 mx-auto\">\n        <div class=\"card-header\"><h3>{{ table_name }}</h3></div>\n        <div class=\"card-body\">\n          <table class=\"table table-striped table-responsive-lg\">\n              <thead>\n                <tr>\n                    <th scope=\"col\">#</th>\n                    <th scope=\"col\">{{ item_name }}</th>\n                    <th scope=\"col\">OctoBots</th>\n                </tr>\n              </thead>\n              <tbody>\n                {% for item in items %}\n                    {{ m_tables.top_tr(item) }}\n                {% endfor %}\n              </tbody>\n          </table>\n        </div>\n    </div>\n{%- endmacro %}\n\n{% macro col_elem_with_badge(title, badge, badge_color=\"unique-color-dark\") -%}\n    <div class=\"col-9 col-md-2 my-1\">\n        {{ title }}\n    </div>\n    <div class=\"col-3 col-md-2 my-1\">\n        <span class=\"badge {{ badge_color }} font-weight-bold\">{{ badge }}</span>\n    </div>\n{%- endmacro %}\n\n{% block body %}\n<br>\n{% if can_get_metrics %}\n    <div class=\"card\">\n        <div class=\"card-header\">\n            <h2>\n                OctoBot Community metrics\n                <a class=\"badge unique-color-dark float-right\" href=\"https://t.me/joinchat/F9cyfxV97ZOaXQ47H5dRWw\" target=\"_blank\" rel=\"noopener\">\n                    <i class=\"fab fa-telegram\"></i> <span class=\"d-none d-md-inline-block\">Community chat</span>\n                </a>\n            </h2>\n        </div>\n    </div>\n    <br>\n    <div class=\"col card text-center align-self-start\">\n        <div class=\"card-header\"><h3>Active OctoBots</h3></div>\n        <h4>\n            <div class=\"card-body row\">\n                {{ col_elem_with_badge(\"Total:\", community_metrics['total_count']) }}\n                {{ col_elem_with_badge(\"This month:\", community_metrics['this_month'], badge_color=\"unique-color\") }}\n                {{ col_elem_with_badge(\"Last 6 months:\", community_metrics['last_six_month']) }}\n            </div>\n        </h4>\n    </div>\n    <br>\n    <div>\n        <div class=\"card my-1\">\n            <div class=\"card-header\">\n                <ul class=\"nav nav-tabs md-tabs justify-content-center\" id=\"tabs\" role=\"tablist\">\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link primary-tab-selector active show\" id=\"real-tab\" data-toggle=\"tab\" href=\"#real\" role=\"tab\"\n                           aria-controls=\"real\"\n                           aria-selected=\"true\">\n                            <h5>Real trading OctoBots this month</h5>\n                        </a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link primary-tab-selector\" id=\"simulated-tab\" data-toggle=\"tab\" href=\"#simulated\" role=\"tab\"\n                           aria-controls=\"simulated\"\n                           aria-selected=\"false\">\n                            <h5>Simulated trading OctoBots this month</h5>\n                        </a>\n                    </li>\n                </ul>\n            </div>\n            <div class=\"tab-content my-2\" id=\"tabcontent\">\n                <div class=\"tab-pane fade show active\" id=\"real\" role=\"tabreal\" aria-labelledby=\"real-tab\">\n                    <div class=\"row w-100 mx-0 d-block d-md-flex\">\n                        {{ top_table_card(community_metrics['top_real_exchanges'], \"Exchange\", \"Top community exchanges\") }}\n                        {{ top_table_card(community_metrics['top_real_eval_config'], \"Tentacle\", \"Top community tentacles\") }}\n                        {{ top_table_card(community_metrics['top_real_pairs'], \"Pair\", \"Top community traded pairs\") }}\n                    </div>\n                </div>\n                <div class=\"tab-pane fade\" id=\"simulated\" role=\"tabsimulated\" aria-labelledby=\"simulated-tab\">\n                    <div class=\"row w-100 mx-0 d-block d-md-flex\">\n                        {{ top_table_card(community_metrics['top_simulated_exchanges'], \"Exchange\", \"Top community exchanges\") }}\n                        {{ top_table_card(community_metrics['top_simulated_eval_config'], \"Tentacle\", \"Top community tentacles\") }}\n                        {{ top_table_card(community_metrics['top_simulated_pairs'], \"Pair\", \"Top community traded pairs\") }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n{% else %}\n    <div class=\"card\">\n        <div class=\"card-header\"><h2>To be part of the OctoBot community, please enable <a href=\"{{ url_for('about') }}\"> OctoBot community data</a>.</h2></div>\n    </div>\n{% endif %}\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/community.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/community_register.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"community\" %}\n\n{% import \"components/community/bots_stats.html\" as bots_stats %}\n{% import \"components/community/login.html\" as login %}\n\n{% block body %}\n<br>\n{{ bots_stats.bots_stats_card(current_bots_stats) }}\n\n<div class=\"login_box mx-auto mt-1 mt-xl-5\">\n    <div class=\"card\">\n        {{ login.register_form(form, is_in_stating_community_env, next_url) }}\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/community.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/bot_selector.html",
    "content": "{% macro bot_selector(all_user_bots, selected_user_bot) -%}\n<div>\n    <h4>\n        Multiple bots available on this account, please select the one to use on this OctoBot.\n    </h4>\n    <div id=\"bot-selector\" data-update-url=\"{{ url_for('api.select_bot') }}\">\n        {% for bot in all_user_bots %}\n        <div class=\"row my-2\">\n            <div class=\"col-8\">\n                {{ bot[\"name\"] }}\n            </div>\n            <div class=\"col-4\">\n                {% if selected_user_bot[\"id\"] == bot[\"id\"] %}\n                <button class=\"btn btn-primary btn-sm waves-effect w-75\"\n                        data-role=\"selected-bot\"\n                        disabled>\n                    <i class='fa fa-check'></i>\n                </button>\n                {% else %}\n                <button class=\"btn btn-primary btn-sm waves-effect w-75 px-1 px-md-4\"\n                        data-role=\"select-bot\"\n                        data-bot-id=\"{{ bot['id'] }}\">\n                    Select\n                </button>\n                {% endif %}\n            </div>\n        </div>\n        {% endfor %}\n\n    </div>\n    <div class=\"alert alert-info\">\n        Bots allow you to identify and load data about a particular OctoBot through your Community account.\n        <br>\n        <span class=\"font-weight-bold\">\n            Never use the same bot on multiple OctoBots at the same time as it will prevent\n            it from working properly.\n        </span>\n    </div>\n    <div class=\"text-center\">\n        <button class=\"btn btn-outline-primary waves-effect\"\n                id=\"create-new-bot\"\n                data-update-url=\"{{ url_for('api.create_bot') }}\">\n            Create a new bot\n        </button>\n    </div>\n</div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/bots_stats.html",
    "content": "{% macro bots_stats_card(stats) -%}\n<div class=\"badge badge-info w-100\">\n    <div>\n        <div class=\"d-flex flex-wrap justify-content-center my-2 w-100\">\n            <div class=\"community-bot-stats-label my-auto py-1 mx-2 mx-md-4\">\n                Welcome to the OctoBot community and its\n                <span class=\"badge community-bot-stats\" id=\"all-bots\">{{ stats[\"total_bots\"] }}</span>\n                already installed OctoBots\n            </div>\n<!--            <div class=\"community-bot-stats-label my-auto py-1 mx-2 mx-md-4\">-->\n<!--                <a class=\"btn btn-outline-danger waves-effect\" href=\"{{url_for('community_metrics')}}\">See more</a>-->\n<!--            </div>-->\n        </div>\n    </div>\n</div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/cloud_strategies.html",
    "content": "{% import 'components/community/cloud_strategies_selector.html' as m_cloud_strategies_selector %}\n{% import 'components/community/octobot_cloud_description.html' as octobot_cloud_description %}\n{% import \"components/community/octobot_cloud_features.html\" as octobot_cloud_features %}\n\n{% macro cloud_strategies(strategies, OCTOBOT_COMMUNITY_URL, LOCALE, OCTOBOT_EXTENSION_PACKAGE_1_NAME, has_open_source_package) -%}\n<div class=\"card\">\n    <div class=\"card-header justify-content-between flex-wrap\">\n        <h2>\n            OctoBot cloud strategies\n        </h2>\n        <div class=\"card-body row mx-0 py-0\">\n            <div class=\"col-12 col-md-2 col-lg-1 py-3\">\n                <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=community_logo\">\n                    <img class=\"img-fluid cloud-logo\"\n                         src=\"{{url_for('static', filename='img/community/octobot-cloud.png')}}\" alt=\"OctoBot cloud\">\n                </a>\n            </div>\n            <div class=\"col-8 col-lg-7 col-xl-4 card-text py-3\">\n                {{ octobot_cloud_features.octobot_cloud_features(OCTOBOT_COMMUNITY_URL, LOCALE, 'community') }}\n            </div>\n            <div class=\"d-none d-xl-flex col-2\">\n                <a class=\"mx-auto\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=community_illustration\">\n                    <img class=\"img-fluid cloud-logo-2x\"\n                         src=\"{{url_for('static', filename='img/community/cloud_dark.png')}}\" alt=\"OctoBot cloud\">\n                </a>\n            </div>\n            <div class=\"col-12 col-lg-4 col-xl-5 card-text py-3\">\n                <p>\n                    Find more information the recent changes and future plans on\n                    <a href=\"{{OCTOBOT_COMMUNITY_URL}}/en/blog?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=community_blog\" target=\"_blank\" rel=\"noopener\"> our blog</a>.\n                </p>\n                <p>\n                    Explore the OctoBot cloud strategies on <a href=\"{{OCTOBOT_COMMUNITY_URL}}/explore?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=community_strategies\" target=\"_blank\" rel=\"noopener\">www.octobot.cloud/explore</a>.\n                </p>\n\n                <p>\n                    Do you want increase the capabilities your OctoBot while supporting the project?\n                    Check out the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}.\n                </p>\n\n                <a href=\"{{ url_for('extensions') }}\"\n                   class=\"btn btn-sm btn-primary waves-effect\">\n                    <i class=\"fa fa-arrow-right\"></i> View the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}\n                </a>\n            </div>\n        </div>\n    </div>\n    <div class=\"card-body\">\n        {{ m_cloud_strategies_selector.cloud_strategies_selector(strategies, LOCALE, \"\") }}\n    </div>\n    {% if not has_open_source_package() %}\n    <div class=\"card-footer text-center\">\n        <i class=\"fa fa-info-circle\"></i>\n        OctoBot cloud <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/features/crypto-basket?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=community_package_baskets_text\">\n            crypto baskets\n        </a> can also be used with your OctoBot when using the <a href=\"{{ url_for('extensions') }}\">{{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}</a>.\n    </div>\n    {% endif %}\n</div>\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/cloud_strategies_selector.html",
    "content": "{% macro cloud_strategies_selector(strategies, locale, post_install_action) -%}\n<div id=\"strategies\">\n    <table class=\"table table-striped table-responsive-md 'table-hover'}} w-100\">\n        <thead>\n            <tr class=\"\">\n                <th scope=\"col\">Strategy</th>\n                <th scope=\"col\">Profitability</th>\n                <th scope=\"col\">Traded coins</th>\n                <th scope=\"col\">Exchange</th>\n                <th scope=\"col\">Risk level</th>\n                <th scope=\"col\">Type</th>\n                <th scope=\"col\">{{'Use' if post_install_action else 'Install'}}</th>\n            </tr>\n        </thead>\n        <tbody>\n        {% for strategy in strategies %}\n            <tr>\n                <td>\n                    <a href=\"{{ strategy.get_product_url() }}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_strategies_selector_strategy_name\" target=\"_blank\">\n                        <div class=\"row\">\n                            <div class=\"col-4 pr-1\">\n                                <img class=\"img-fluid package_row_image\"\n                                     src=\"{{strategy.get_logo_url(url_for('static', filename='img/community/tentacles_packages_previews/'))}}\"\n                                     alt=\"Strategy illustration\">\n                            </div>\n                            <div class=\"col-8 text-left my-auto pl-0 font-weight-bold\">\n                                {{strategy.get_name(locale) | capitalize}}\n                                <i class=\"fas fa-external-link-alt\"></i>\n                            </div>\n                        </div>\n                    </a>\n                </td>\n                {% if strategy.results %}\n                <td class=\"align-middle\" data-order=\"{{ strategy.results.get_max_value() }}\">\n                    {{ (strategy.results.get_max_value()) | round(2) }}% over {{ strategy.results.get_max_unit() }}\n                </td>\n                {% else %}\n                <td data-order=\"0\"></td>\n                {% endif %}\n                <td class=\"align-middle\">\n                    {{ strategy.attributes['coins'] | join(', ') }}\n                </td>\n                <td class=\"align-middle\">\n                    {{ strategy.attributes['exchanges'] | join(', ') }}\n                </td>\n                <td class=\"align-middle\">\n                    {{ strategy.get_risk().name | capitalize }}\n                </td>\n                <td class=\"align-middle\">\n                    {% if strategy.category %}\n                        {% if strategy.category.get_url() %}\n                            <a href=\"{{strategy.category.get_url()}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_strategies_selector_strategy_category\" target=\"_blank\">\n                                {{ strategy.category.get_name(locale) }} <i class=\"fas fa-external-link-alt\"></i>\n                            </a>\n                        {% else %}\n                            {{ strategy.category.get_name(locale) }}\n                        {% endif %}\n                    {% endif %}\n                </td>\n                <td class=\"align-middle\">\n                    <button update-url=\"{{ url_for('profiles_management', action='download') }}\"\n                            role=\"button\" class=\"btn btn-sm btn-outline-primary px-1 m-1\"\n                            data-toggle=\"tooltip\" data-placement=\"top\" title=\"{{'Use this strategy' if post_install_action else 'Add to OctoBot'}}\"\n                            data-role=\"install-strategy\"\n                            data-strategy-id=\"{{strategy.id}}\"\n                            data-strategy-name=\"{{strategy.get_name(locale) | capitalize}}\"\n                            data-description=\"{{strategy.content['description_translations'][locale]}}\"\n                            data-post-install-action=\"{{post_install_action}}\"\n                    >\n                        <i class=\"fa {{'fa-check' if post_install_action else 'fa-download'}} px-2 strategy_action px-4\"></i>\n                    </button>\n                </td>\n            </tr>\n        {% endfor %}\n        </tbody>\n    </table>\n</div>\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/login.html",
    "content": "{% from \"macros/forms.html\" import render_field %}\n\n{% macro login_form(form, is_in_stating_community_env, after_login_url, recover_password_url,\n                    after_login_action=None, details=None) -%}\n<div class=\"text-center\">\n    <div class=\"card-header\">\n        <h2>\n            OctoBot community\n        </h2>\n    </div>\n    <div class=\"card-body\">\n        {% with messages = get_flashed_messages(with_categories=true) %}\n          {% if messages %}\n                {% for category, message in messages %}\n                <div class=\"alert alert-{{ 'danger' if category == 'error' else 'success' }}\">\n                    {{ message }}\n                </div>\n                {% endfor %}\n          {% endif %}\n        {% endwith %}\n        {% if current_logged_in_email %}\n            <h5>Logged in as {{ current_logged_in_email }}</h5>\n            <a class=\"btn btn-sm btn-outline-warning waves-effect mt-4\" href=\"{{ url_for('community_logout') }}\">Logout</a>\n        {% else %}\n            <h5>\n                Login and access your\n                {% if is_in_stating_community_env() %}\n                    <span class=\"badge badge-light\">\n                        Beta\n                    </span>\n                {% endif %}\n                Octobot account.\n            </h5>\n            {% if details %}\n            <p>\n                {{ details }}\n            </p>\n            {% endif %}\n            {{ login_form_content(form, 'community_login', after_login_url, after_login_action, \"Login\") }}\n            <div class=\"mt-2\">\n                <a href=\"{{ recover_password_url }}\" class=\"font-weight-bold\">Forgot your password ?</a>\n            </div>\n        {% endif %}\n    </div>\n    {% if not current_logged_in_email %}\n        <div class=\"card-footer mx-auto\">\n            New to the OctoBot community ? <a href=\"{{ url_for('community_register', next=after_login_url) }}\" class=\"font-weight-bold\">Create your account here.</a>\n        </div>\n    {% endif %}\n</div>\n{%- endmacro %}\n\n{% macro register_form(form, is_in_stating_community_env, after_login_url,\n                    after_login_action=None, details=None) -%}\n<div class=\"text-center\">\n    <div class=\"card-header\">\n        <h2>\n            Join the OctoBot community\n        </h2>\n    </div>\n    <div class=\"card-body\">\n        {% with messages = get_flashed_messages(with_categories=true) %}\n          {% if messages %}\n                {% for category, message in messages %}\n                <div class=\"alert alert-{{ 'danger' if category == 'error' else 'success' }}\">\n                    {{ message }}\n                </div>\n                {% endfor %}\n          {% endif %}\n        {% endwith %}\n        <h5>\n            Create your\n            {% if is_in_stating_community_env() %}\n                <span class=\"badge badge-light\">\n                    Beta\n                </span>\n            {% endif %}\n            Octobot account.\n        </h5>\n        {% if details %}\n        <p>\n            {{ details }}\n        </p>\n        {% endif %}\n        {{ login_form_content(form, 'community_register', after_login_url, after_login_action, \"Register\") }}\n    </div>\n    <div class=\"card-footer\">\n        Already have an OctoBot account ? <a href=\"{{ url_for('community_login', next=after_login_url) }}\" class=\"font-weight-bold\">Login here.</a>\n    </div>\n</div>\n{%- endmacro %}\n\n{%- macro login_form_content(form, submit_route, after_login_url, after_login_action, form_value) %}\n<form method=post action=\"{{url_for(submit_route, next=after_login_url, after_login_action=after_login_action)}}\" name=\"community-login\">\n    <div class=\"my-4\">\n        {{ form.csrf_token }}\n        <div class=\"mb-2\">\n            {{ render_field(form.email, autofocus=true, class=\"form-control mx-auto\", placeholder=\"Email\") }}\n        </div>\n        <div>\n            {{ render_field(form.password, autofocus=true, class=\"form-control mx-auto\", placeholder=\"Password\") }}\n        </div>\n        <div class=\"custom-control custom-switch mt-2\">\n            {{ render_field(form.remember_me, class=\"custom-control-input\") }}\n            <label class=\"custom-control-label\" for=\"remember_me\">Remember me</label>\n        </div>\n    </div>\n    <span class=\"mt-2\">\n        <input type=submit value={{form_value}} class=\"btn btn-primary waves-effect m-0\">\n        <span id=\"login-waiter\" class=\"d-none btn btn-outline-primary mt-2\" disabled><i class='fa fa-spinner fa-spin'></i></span>\n    </span>\n</form>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/octobot_cloud_description.html",
    "content": "{% import \"components/community/octobot_cloud_features.html\" as octobot_cloud_features %}\n\n\n{% macro octobot_cloud_description(OCTOBOT_COMMUNITY_URL, LOCALE) -%}\n<div class=\"row\">\n    <div class=\"col-12 col-md-2 col-xl-1 d-flex\">\n        <div class=\"m-auto\">\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=about_logo\">\n                <img class=\"img-fluid cloud-logo-4x\"\n                     src=\"{{url_for('static', filename='img/community/octobot-cloud.png')}}\" alt=\"OctoBot cloud\">\n            </a>\n        </div>\n    </div>\n    <div class=\"col-12 col-md-10 col-lg-6 col-xl-7 pt-4\">\n        <h5>Profit from OctoBot strategies in a simpler way</h5>\n        <p>\n            While the <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}/trading-bot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=about\">OctoBot</a>\n             you are currently using is about creating and testing your own strategy,\n            <a href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=about\" target=\"_blank\" rel=\"noopener\">\n                octobot.cloud</a>, enables every crypto investors to enjoy trading strategies in the simplest way possible. <br/>\n            It also uses OctoBot under the hood and is perfect to diversify crypto investments or for people who don't want the technical aspect of a trading bot.\n       </p>\n        {{ octobot_cloud_features.octobot_cloud_features(OCTOBOT_COMMUNITY_URL, LOCALE, 'about') }}\n        <p>\n            Follow the OctoBot news on\n            <a href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/blog/introducing-the-new-octobot-cloud?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=about\" target=\"_blank\" rel=\"noopener\"> our blog</a>.\n        </p>\n    </div>\n    <div class=\"d-none d-lg-flex col-lg-4\">\n        <a class=\"mx-auto\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=about_illustration\">\n            <img class=\"img-fluid cloud-logo-4x\"\n                 src=\"{{url_for('static', filename='img/community/cloud_dark.png')}}\" alt=\"OctoBot cloud\">\n        </a>\n    </div>\n</div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/octobot_cloud_features.html",
    "content": "{% macro octobot_cloud_features(OCTOBOT_COMMUNITY_URL, LOCALE, source) -%}\n<div>\n    With <a href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content={{source}}_community_link\" target=\"_blank\" rel=\"noopener\">OctoBot cloud</a>,\n    you can:\n    <ul>\n        <li>\n            Use crypto investment and\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content={{source}}_strategies\">\n                trading strategies\n            </a>\n            in the <strong>simplest way possible for free</strong>.\n        </li>\n        <li>\n            Invest in themes such as <strong>AI, RWA, Meme coins</strong> or even <strong>top crypto of the market </strong> using <a href=\"{{OCTOBOT_COMMUNITY_URL}}/features/crypto-basket?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content={{source}}_baskets_link\" target=\"_blank\" rel=\"noopener\">crypto baskets</a>.\n        </li>\n        <li>\n             Easily <a href=\"{{OCTOBOT_COMMUNITY_URL}}/features/tradingview-bot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content={{source}}_tradingview_link\" target=\"_blank\" rel=\"noopener\">automate your TradingView strategies</a>.\n        </li>\n        <li>\n            <strong>Create strategies</strong> by describing them to\n            <a href=\"{{OCTOBOT_COMMUNITY_URL}}/creator?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content={{source}}_octobot_ai_link\" target=\"_blank\" rel=\"noopener\">\n            OctoBot AI</a> and execute them on your cloud or local OctoBot.\n        </li>\n    </ul>\n</div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/tentacle_packages.html",
    "content": "{% macro pending_tentacles_install_modal(show_by_default) -%}\n    <div class=\"modal\" id=\"pending-tentacles-install-modal\" tabindex=\"-1\" role=\"dialog\"\n         aria-hidden=\"true\"\n         data-show-by-default=\"{{show_by_default}}\">\n      <div class=\"modal-dialog modal-dialog-centered modal-md\" role=\"document\">\n        <div class=\"modal-content modal-text\">\n          <div class=\"modal-header primary-text\">\n            <h5 class=\"modal-title\">New tentacles are available!</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body text-center\">\n              <div class=\"fs-1 my-2\">\n                  🎉\n              </div>\n              <p>\n                  Your OctoBot has to restart to install your new tentacles.\n                  Please restart your OctoBot.\n              </p>\n          </div>\n          <div class=\"modal-footer\">\n            <button type=\"button\" class=\"btn btn-outline-primary\" data-dismiss=\"modal\">\n              Cancel\n            </button>\n            <button type=\"button\" class=\"btn btn-primary\" data-dismiss=\"modal\" route=\"{{ url_for('commands', cmd='restart') }}\">\n              Restart and install\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n{%- endmacro %}\n\n{% macro waiting_for_owned_packages_to_install_modal(show_by_default) -%}\n    <div class=\"modal\" id=\"waiting-for-owned-packages-to-install-modal\" tabindex=\"-1\" role=\"dialog\"\n         aria-hidden=\"true\"\n         data-backdrop=\"static\"\n         data-keyboard=\"false\"\n         data-show-by-default=\"{{show_by_default}}\"\n         data-url=\"{{url_for('api.has_open_source_package')}}\">\n      <div class=\"modal-dialog modal-dialog-centered modal-md\" role=\"document\">\n        <div class=\"modal-content modal-text\">\n          <div class=\"modal-header primary-text\">\n            <h5 class=\"modal-title\">Waiting for payment to succeed</h5>\n          </div>\n          <div class=\"modal-body\">\n              <div class=\"text-center my-5\">\n                  <i class='fa fa-2xl fa-circle-notch fa-spin'></i>\n              </div>\n              <p>\n                  Thank you for your purchase. You will be able to install your extension as\n                  soon as your payment is confirmed.\n              </p>\n          </div>\n          <div class=\"modal-footer\">\n              <p>\n                  <i class=\"fa-brands fa-ethereum\"></i> Note: when paying in crypto, it might take a few minutes\n              </p>\n          </div>\n        </div>\n      </div>\n    </div>\n{%- endmacro %}\n\n{% macro select_payment_method_modal(name) -%}\n    <div class=\"modal\" id=\"select-payment-method-modal\" tabindex=\"-1\" role=\"dialog\"\n         aria-hidden=\"true\">\n      <div class=\"modal-dialog modal-dialog-centered modal-md\" role=\"document\">\n        <div class=\"modal-content modal-text\">\n          <div class=\"modal-header primary-text\">\n            <h5 class=\"modal-title\">Select your payment method</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body text-center\">\n              <p>\n                  Please choose how you want to pay for your {{name}}\n              </p>\n              <p>\n                <button type=\"button\" class=\"btn btn-outline-primary\"\n                        data-role=\"open-checkout\" data-payment-method=\"credit-card\" data-checkout-api-url=\"{{url_for('api.checkout_url')}}\">\n                    <i class=\"fa-solid fa-credit-card mr-2\"></i> Pay by credit card\n                </button>\n              </p>\n              <p>\n                <button type=\"button\" class=\"btn btn-outline-primary disabled\" data-role=\"open-checkout\" data-payment-method=\"crypto\" data-checkout-api-url=\"{{url_for('api.checkout_url')}}\">\n                    <i class=\"fa-brands fa-ethereum mr-2\"></i> Pay with crypto (coming back soon)\n                </button>\n              </p>\n              <p class=\"d-none\" data-role=\"checkout-url-fallback-part\">\n                If this page is not refreshing, please click on the following link to pay for your {{name}}:\n                <a data-role=\"checkout-url-fallback\" href=\"\" class=\"text-break\"></a>\n              </p>\n          </div>\n          <div class=\"modal-footer\">\n            <button type=\"button\" class=\"btn btn-outline-primary\" data-dismiss=\"modal\">\n              Cancel\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n{%- endmacro %}\n\n{% macro get_package_button(name, is_authenticated, has_open_source_package) -%}\n    {% if has_open_source_package() %}\n    <button type=\"button\" class=\"btn btn-outline-primary disabled\">\n        {{name}} already purchased\n    </button>\n    {% elif is_authenticated %}\n    <button type=\"button\" class=\"btn btn-primary btn-lg\"\n            data-role=\"open-package-purchase\">\n        Get your {{name}}\n    </button>\n    {% else %}\n    <a type=\"button\" class=\"btn btn-primary btn-lg\"\n       href=\"{{ url_for('community_login', next='extensions') }}\">\n        Login to install the {{name}}\n    </a>\n    {% endif %}\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/community/user_details.html",
    "content": "{% macro user_details(\n    USER_EMAIL,\n    USER_SELECTED_BOT_ID,\n    has_open_source_package,\n    PROFILE_NAME,\n    TRADING_MODE_NAME,\n    EXCHANGE_NAMES,\n    IS_REAL_TRADING)\n-%}\n<script>\n    const _USER_DETAILS = {\n        email: \"{{ USER_EMAIL }}\",\n        botId: \"{{ USER_SELECTED_BOT_ID }}\",\n        hasPremiumExtension: {{ 'true' if has_open_source_package() else 'false'}},\n        profileName: \"{{ PROFILE_NAME }}\",\n        tradingMode: \"{{ TRADING_MODE_NAME }}\",\n        exchanges: \"{{ EXCHANGE_NAMES }}\".split(\",\"),\n        isRealTrading: {{ 'true' if IS_REAL_TRADING else 'false' }},\n    }\n</script>\n{%- endmacro %}\n\n{%- macro posthog(IS_DEMO, IS_CLOUD, IS_ALLOWING_TRACKING, PH_TRACKING_ID) -%}\n    <script src=\"{{ url_for('static', filename='js/common/tracking.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    {% if IS_DEMO or IS_CLOUD or IS_ALLOWING_TRACKING%}\n    <script>\n        !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(\".\");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement(\"script\")).type=\"text/javascript\",p.crossOrigin=\"anonymous\",p.async=!0,p.src=s.api_host.replace(\".i.posthog.com\",\"-assets.i.posthog.com\")+\"/static/array.js\",(r=t.getElementsByTagName(\"script\")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a=\"posthog\",u.people=u.people||[],u.toString=function(t){var e=\"posthog\";return\"posthog\"!==a&&(e+=\".\"+a),t||(e+=\" (stub)\"),e},u.people.toString=function(){return u.toString(1)+\".people (stub)\"},o=\"init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug\".split(\" \"),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);\n        const _IS_ALLOWING_TRACKING = true\n        posthog.init('{{PH_TRACKING_ID}}',{\n            api_host:'https://eu.i.posthog.com', \n            person_profiles: 'always', // 'always' to create profiles for anonymous users as well\n            defaults:'2025-05-24',\n            loaded: function (posthog) {\n                posthog_loaded(posthog)\n            }\n        })\n    </script>\n    {% else %}\n    <script>\n        const _IS_ALLOWING_TRACKING = false\n    </script>\n    {% endif %}\n{%- endmacro -%}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/currency_card.html",
    "content": "{% import 'components/config/editable_config.html' as m_editable_config %}\n{% macro config_currency_card(config_symbols, crypto_currency, symbol_list_by_type, full_symbol_list, get_currency_id, add_class='', no_select=False, additional_classes=\"\", symbol=\"\") -%}\n    <!-- Card -->\n    <div class=\"card {{add_class}} mb-3 medium-size config-card\">\n\n        <!--Title-->\n        <div class=\"card-header\">{{crypto_currency}}</div>\n\n        <!--Card image-->\n        <div class=\"view overlay animated fadeIn\">\n            <img class=\"card-img-top currency-image p-2 {{additional_classes}}\"\n                 src=\"{{ url_for('static', filename='img/svg/loading_currency.svg') }}\"\n                 alt=\"{{ crypto_currency }}\"\n                 data-currency-id=\"{{get_currency_id(full_symbol_list, crypto_currency)}}\"\n                 style=\"height:100%\">\n        </div>\n\n        <!--Card content-->\n        <div class=\"card-body\" name=\"{{crypto_currency}}\" config-key=\"crypto-currencies_{{crypto_currency}}\">\n\n            <p class=\"card-text symbols my-0 {{additional_classes}}\">\n                 {{ m_editable_config.editable_key( config_symbols,\n                                                    crypto_currency,\n                                                    \"crypto-currencies_\" + crypto_currency,\n                                                    \"global_config\",\n                                                    config_symbols[crypto_currency]['pairs'] if crypto_currency in config_symbols and 'pair' in config_symbols[crypto_currency] else [],\n                                                    config_symbols[crypto_currency]['pairs'] if crypto_currency in config_symbols and 'pair' in config_symbols[crypto_currency] else [],\n                                                    symbol_list_by_type,\n                                                    no_select,\n                                                    identifier=crypto_currency,\n                                                    placeholder_str=\"Select trading pair(s)\",\n                                                    dict_as_option_group=True)\n                }}\n            </p>\n\n            <button type=\"button\" class=\"btn btn-danger remove-btn px-3 waves-effect\"><i class=\"fa fa-ban\" aria-hidden=\"true\"></i> Remove</button>\n        </div>\n    </div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/editable_config.html",
    "content": "{% macro editable_key(config, key, config_key, config_type, config_value, startup_config_value, suggestions=\"\",\n                      no_select=False, number_step=0.01, force_title=False, tooltip=None, identifier=\"\",\n                      placeholder_str=\"\", allow_create_for=None, edit_key=False, dict_as_option_group=False) -%}\n    <span\n        {% if tooltip is not none and not config[key] | is_bool %}\n            data-toggle=\"tooltip\" title=\"{{tooltip}}\"\n        {% endif %}\n    >\n        {% if config[key]|default (config_value) is string %}\n            {{ editable_key_string(config, key, config_key, config_type, config_value, startup_config_value, suggestions, placeholder_str) }}\n        {% elif config[key]|default (config_value) | is_dict %}\n            {{ editable_key_dict(config, key, config_key, config_type, config_value, startup_config_value, suggestions, no_select, number_step, force_title, tooltip, identifier, placeholder_str, allow_create_for, dict_as_option_group=dict_as_option_group) }}\n        {% elif config[key]|default (config_value) | is_list %}\n            {{ editable_key_list(config, key, config_key, config_type, config_value, startup_config_value, suggestions, no_select, force_title, identifier, placeholder_str, dict_as_option_group=dict_as_option_group) }}\n        {% elif config[key]|default (config_value) | is_bool %}\n            {{ editable_key_bool(config, key, config_key, config_type, config_value, startup_config_value, suggestions, tooltip) }}\n        {% elif config[key]|default (config_value) is number %}\n            {{ editable_key_number(config, key, config_key, config_type, config_value, startup_config_value, suggestions, number_step, edit_key) }}\n        {% else %}\n            {{ editable_key_string(config, key, config_key, config_type, config_value, startup_config_value, suggestions, placeholder_str) }}\n        {% endif %}\n    </span>\n{%- endmacro %}\n\n{% macro editable_key_dict(config, key, config_key, config_type, config_value, startup_config_value, suggestions, no_select, number_step, force_title, tooltip, identifier, placeholder_str, allow_create_for, dict_as_option_group) -%}\n    <span class=\"text-capitalize\">{{ key }} : </span> <br>\n    {% for new_key in config[key] %}\n\n        &emsp;{{  editable_key( config[key],\n                            new_key,\n                            config_key + \"_\" + new_key,\n                            config_type,\n                            config_value[new_key],\n                            startup_config_value[new_key],\n                            suggestions,\n                            no_select,\n                            number_step,\n                            force_title,\n                            tooltip,\n                            identifier,\n                            placeholder_str,\n                            edit_key=(allow_create_for == key),\n                            dict_as_option_group=dict_as_option_group)\n        }}\n        {% if loop.last and allow_create_for == key %}\n            <span data-add-template-for=\"{{config_key + '_Empty'}}\" class=\"d-none\">\n                {{  editable_key( config[key],\n                                \"Empty\",\n                                config_key + \"_Empty\",\n                                config_type,\n                                config_value[new_key],\n                                startup_config_value[new_key],\n                                suggestions,\n                                no_select,\n                                number_step,\n                                force_title,\n                                tooltip,\n                                identifier,\n                                placeholder_str,\n                                edit_key=True,\n                                dict_as_option_group=dict_as_option_group)\n                }}\n            </span>\n            <button type=\"button\" class=\"btn btn-sm btn-primary rounded-circle waves-effect ml-4\"\n                    data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"Add a new element to {{key}}\"\n                    data-role=\"editable-add\" data-add-template-target=\"{{config_key + '_Empty'}}\"\n                    data-default-key=\"Empty\">\n                <i class=\"fas fa-plus\"></i>\n            </button><br>\n        {% endif %}\n    {% endfor %}\n{%- endmacro %}\n\n{% macro editable_key_number(config, key, config_key, config_type, config_value, startup_config_value, suggestions, number_step, edit_key) -%}\n    <span class=\"text-capitalize\">\n        {% if edit_key %}\n            <a href=\"#\"\n               config-type=\"global_config\"\n               config-value=\"{{ key }}\"\n               startup-config-value=\"{{ key }}\"\n               data-type=\"text\"\n               data-pk=\"1\"\n               data-clear=false\n               data-onblur=\"submit\"\n               data-highlight=true\n               data-label-for=\"{{config_key}}\"\n               placeholder=\"{{ key }}\"\n               class=\"editable editable-click config-element\">\n            {{ key }}</a>\n        {% else %}\n            {{ key }}\n        {% endif %}:\n    </span>\n    <a href=\"#\"\n                   config-key=\"{{config_key}}\"\n                   config-type=\"global_config\"\n                   config-value=\"{{config[key]}}\"\n                   startup-config-value=\"{{config[key]}}\"\n                   data-type=\"number\"\n                   data-step=\"{{number_step}}\"\n                   data-pk=\"1\"\n                   data-onblur=\"submit\"\n                   data-highlight=true\n                   id=\"{{config_key}}\"\n                class=\"editable editable-click config-element\">\n    {{ config[key]|default (config_value) }}</a><br>\n{%- endmacro %}\n\n{% macro editable_key_string(config, key, config_key, config_type, config_value, startup_config_value, suggestions, placeholder_str=\"\", password_val=\"*********\") -%}\n    <span class=\"text-capitalize\">{{ key }} : </span>\n    {% if key == \"password\" %}\n        <a href=\"#\"\n           config-key=\"{{config_key}}\"\n           config-type=\"global_config\"\n           config-value=\"{{password_val}}\"\n           startup-config-value=\"{{password_val}}\"\n           data-type=\"text\"\n           data-pk=\"1\"\n           data-clear=false\n           data-onblur=\"submit\"\n           data-highlight=true\n           id=\"{{config_key}}\"\n           placeholder=\"{{ placeholder_str }}\"\n           class=\"editable editable-click config-element\">\n        {{ password_val }}</a><br>\n    {% else %}\n        <a href=\"#\"\n           config-key=\"{{config_key}}\"\n           config-type=\"global_config\"\n           config-value=\"{{config[key]}}\"\n           startup-config-value=\"{{config[key]}}\"\n           data-type=\"text\"\n           data-pk=\"1\"\n           data-clear=false\n           data-onblur=\"submit\"\n           data-highlight=true\n           id=\"{{config_key}}\"\n           placeholder=\"{{ placeholder_str }}\"\n           class=\"editable editable-click config-element\">\n        {{ config[key]|default (config_value) }}</a><br>\n    {% endif %}\n{%- endmacro %}\n\n{% macro editable_key_bool(config, key, config_key, config_type, config_value, startup_config_value, suggestions, tooltip) -%}\n    <div class=\"custom-control custom-switch\">\n      <input type=\"checkbox\"\n             class=\"custom-control-input config-element\"\n             config-key=\"{{config_key}}\"\n             config-type=\"global_config\"\n             config-value=\"{{config[key]}}\"\n             startup-config-value=\"{{config[key]}}\"\n             data-type=\"bool\"\n             id=\"{{config_key}}\"\n      {% if config[key] == True %}checked{% endif %}>\n\n      <label class=\"custom-control-label text-capitalize\" for=\"{{config_key}}\"\n          {% if tooltip is not none %}\n            data-toggle=\"tooltip\" title=\"{{tooltip}}\"\n          {% endif %}\n      >\n\n          {{ key }}\n      </label>\n    </div>\n{%- endmacro %}\n\n{% macro editable_key_list(config, key, config_key, config_type, config_value, startup_config_value, suggestions, no_select, force_title, identifier=\"\", placeholder_str=\"\", dict_as_option_group=False) -%}\n    {% if force_title %}\n        <span class=\"text-capitalize\">{{ key }} : </span>\n    {% endif %}\n    <select config-key=\"{{ config_key }}\"\n            config-type=\"global_config\"\n            config-value=\"{{config[key]}}\"\n            startup-config-value=\"{{config[key]}}\"\n            class=\"editable editable-click config-element multi-select-element\"\n            data-type=\"list\"\n            multiple=\"multiple\"\n            style=\"width:100%;\"\n            id=\"{{config_key}}\"\n            editable_config_id=\"multi-select-element-{{ identifier }}\"\n            >\n        {% if dict_as_option_group and suggestions is mapping %}\n            {% for group_name, values in suggestions.items() %}\n                <optgroup label=\"{{group_name}}\">\n                {% for name in values %}\n                    <option value=\"{{ name }}\"\n                    {% if not no_select %}\n                        {{\"selected=selected\" if name in config[key] else \"\"}}\n                    {% endif %}\n                    >\n                        {{ name }}\n                    </option>\n                {% endfor %}\n               </optgroup>\n            {% endfor %}\n        {% else %}\n            {% for name in config[key] %}\n                <option value=\"{{ name }}\"\n                {% if not no_select %}\n                    selected=\"selected\"\n                {% endif %}\n                >\n                    {{ name }}\n                </option>\n            {% endfor %}\n            {% for name in suggestions %}\n                {% if name not in config[key] %}\n                <option value=\"{{ name }}\">\n                    {{ name }}\n                </option>\n                {% endif %}\n            {% endfor %}\n        {% endif %}\n    </select>\n    {% if not no_select %}\n    <br>\n    <script>\n        $(\"select[editable_config_id=\\\"multi-select-element-{{ identifier }}\\\"]:first\").select2({\n            dropdownAutoWidth : true,\n            tags: true,\n            placeholder:\"{{ placeholder_str }}\"\n        });\n    </script>\n    {% endif %}\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/evaluator_card.html",
    "content": "{% import 'macros/tentacles.html' as m_tentacles %}\n\n{% macro config_evaluator_card(startup_config, evaluator_name, info, config_type, strategy=False, include_modal=True) %}\n    <a href=\"#\" onclick=\"return false;\"\n       class=\"col-md-6 col-lg-4 p-1 rounded list-group-item waves-effect {{ 'd-none' if strategy }} {{'list-group-item-success' if info['activation'] else 'list-group-item-light'}} config-element\"\n       id={{ evaluator_name }} name={{ evaluator_name }}\n       config-type=\"{{config_type}}\" config-key={{evaluator_name}} current-value={{info['activation']}} config-value={{info['activation']}} startup-config-value={{startup_config[evaluator_name]}}\n       requirements=\"{{info['requirements']}}\"\n       default-elements=\"{{info['default-config']}}\"\n       requirements-min-count=\"{{info['requirements-min-count']}}\">\n        {% if not strategy %}\n        <span class=\"float-left {{'d-none' if not info['required']}}\" role=\"required-flag\">\n            <i class=\"fa fa-flag {{'red-text' if not info['activation']}}\"\n               data-toggle=\"tooltip\" data-placement=\"top\"\n               title=\"Should be activated when using the current strategies\">\n            </i>\n        </span>\n        {% endif %}\n        <span class=\"ps-2\">\n            {{ evaluator_name }}\n        </span>\n\n        <span class=\"float-right\">\n            <span class=\"badge {{'badge-warning' if (evaluator_name in startup_config and startup_config[evaluator_name] != info['activation']) else ('badge-success' if info['activation'] else 'badge-secondary')}}\">\n                {{('Activation pending restart' if info['activation'] else 'Deactivation pending restart') if (evaluator_name in startup_config and startup_config[evaluator_name] != info['activation']) else ('Activated' if info['activation'] else 'Deactivated')}}\n            </span>\n            <button class=\"btn btn-outline-primary btn-md waves-effect\" data-toggle=\"modal\" data-target=\"#{{evaluator_name}}Modal\" no-activation-click=\"true\"><i class=\"fas fa-cog\" no-activation-click=\"true\"></i></button>\n        </span>\n    </a>\n    {% if include_modal %}\n        {{ evaluator_card_modal(evaluator_name, info, strategy) }}\n    {% endif %}\n{% endmacro %}\n\n{% macro tentacle_evaluator_card(startup_config, evaluator_name, info, config_type, strategy=False) %}\n    <a href=\"#\" onclick=\"return false;\"\n       class=\"col-md-6 col-lg-4 p-1 rounded list-group-item waves-effect {{'list-group-item-success' if info['activation'] else 'list-group-item-light'}} config-element hover_anim\"\n       id={{evaluator_name}} name={{ evaluator_name }}\n       config-type={{config_type}} config-key={{evaluator_name}} current-value={{info['activation']}} config-value={{info['activation']}} startup-config-value={{startup_config[evaluator_name]}}\n       requirements=\"{{info['requirements']}}\"\n       default-elements=\"{{info['default-config']}}\"\n       requirements-min-count=\"{{info['requirements-min-count']}}\">\n        <span class=\"ps-2\">\n            {{ evaluator_name }}\n        </span>\n        <span class=\"float-right\">\n            <span class=\"badge {{'badge-warning' if startup_config[evaluator_name] != info['activation'] else ('badge-success' if info['activation'] else 'badge-secondary')}}\">\n                {{('Activation pending restart' if info['activation'] else 'Deactivation pending restart') if startup_config[evaluator_name] != info['activation'] else ('Activated' if info['activation'] else 'Deactivated')}}\n            </span>\n            <button class=\"btn btn-outline-primary btn-md waves-effect\" data-toggle=\"modal\" data-target=\"#{{evaluator_name}}Modal\" no-activation-click=\"true\"><i class=\"fa fa-info-circle\" no-activation-click=\"true\"></i></button>\n        </span>\n    </a>\n    {{ evaluator_card_modal(evaluator_name, info, strategy) }}\n{% endmacro %}\n\n{% macro evaluator_card_modal(evaluator_name, info, strategy=False, read_only=False) %}\n    <div class=\"modal fade\" id=\"{{evaluator_name}}Modal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#{{evaluator_name}}ModalLabel\" aria-hidden=\"true\">\n      <div class=\"modal-dialog modal-dialog-centered modal-xl\" role=\"document\">\n        <div class=\"modal-content modal-text mt-4\">\n          <div class=\"modal-header primary-text\">\n              <h5 class=\"modal-title\" id=\"#{{evaluator_name}}ModalLabel\">\n                  {{evaluator_name}}\n              </h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\">\n              {{ m_tentacles.tentacle_description(info, strategy, evaluator_name, read_only) }}\n          </div>\n          {% if not read_only %}\n          <div class=\"modal-footer\">\n            <a href=\"{{ url_for('config_tentacle', name=(evaluator_name)) }}\"\n               class=\"btn btn-primary waves-effect\" role=\"button\">Configure</a>\n            <button class=\"btn btn-outline-primary waves-effect\" data-dismiss=\"modal\">Close</button>\n          </div>\n          {% endif %}\n        </div>\n      </div>\n    </div>\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/exchange_card.html",
    "content": "{% import 'components/config/editable_config.html' as m_editable_config %}\n{% macro config_exchange_card(config, exchange, exchanges_details, is_supporting_future_trading,\n                              keys_value=\"*********\", enabled=True,\n                              sandboxed=False, selected_exchange_type='spot', add_class='', config_values=None,\n                              full_config=True, lite_config=False) -%}\n    <!-- Card -->\n    <div data-role=\"exchange\" class=\"card mb-4 {{add_class}} config-card\">\n\n        <div class=\"card-header d-flex\">\n            <div class=\"col-7 col-lg-5\">\n                <h4 class=\"text-capitalize\">\n                    {{exchange}}\n                    <span data-role=\"supporting-exchange\" class=\"mx-md-4 d-none\">\n                        <i class=\"fas fa-star\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Partner exchange:\n                        Support the OctoBot project by trading on this exchange.\"></i>\n                    </span>\n                    <span data-role=\"valid-account\"\n                          class=\"mx-md-4 d-none\"\n                          data-toggle=\"tooltip\"\n                          data-placement=\"top\"\n                          title=\"Login successful\">\n                        <i class=\"fas fa-check\"></i>\n                    </span>\n                    <span data-role=\"supporting-account\" class=\"mx-md-4 d-none\">\n                        <i class=\"fas fa-trophy\" data-toggle=\"tooltip\" data-placement=\"top\"\n                           title=\"Partner exchange: Thank you for supporting the OctoBot project by using this exchange !\"></i>\n                    </span>\n                </h4>\n            </div>\n            <div class=\"col-5 col-lg-5\">\n                <a href=\"\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"waves-effect\">\n                    <img class=\"img-fluid product-logo exchange-logo d-none\" src=\"\" alt=\"{{exchange}}-logo\" url=\"{{url_for('exchange_logo', name=exchange)}}\">\n                </a>\n            </div>\n            {% if not lite_config %}\n            <div class=\"col-sm-5 col-lg-2 row\">\n                <div data-role=\"websocket-mark\" class=\"{{'' if exchanges_details['has_websockets'] else 'd-none'}} col\">\n                    <div class=\"d-lg-flex justify-content-end\">\n                        <i class=\"fas fa-bolt\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"This exchange can support\n                        many more trading pairs and is faster thanks to its websocket connection.\"></i>\n                    </div>\n                </div>\n                <div class=\"{{'' if exchanges_details['configurable'] else 'd-none'}} col\">\n                    <div class=\"d-lg-flex justify-content-end\">\n                        <a href=\"{{ url_for('config_tentacle', name=(exchange)) }}\">\n                            <i class=\"fas fa-cog\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Configure\"></i>\n                        </a>\n                    </div>\n                </div>\n            </div>\n            {% endif %}\n        </div>\n\n        <!--Card image-->\n        <div class=\"view overlay\">\n          <!--{{ exchange }}-->\n        </div>\n\n        <!--Card content-->\n        <div class=\"card-body px-2 px-md-4\" name=\"{{exchange}}\" config-key=\"exchanges_{{exchange}}\">\n            <div data-role=\"account-warning-details-wrapper\" class=\"alert alert-danger d-none px-1 px-md-4\">\n                <span data-role=\"account-warning-details\">\n                    Unknown authentication error.\n                </span>\n            </div>\n            <div class=\"d-md-flex\">\n                {% if full_config %}\n                    <div class=\"flex-grow-1\">\n                    <p class=\"card-text api analytics-hidden\">\n                        API Key : <a href=\"#\"\n                                     id=\"exchange_api-key\"\n                                     config-key=\"exchanges_{{exchange}}_api-key\"\n                                     config-type=\"global_config\"\n                                     config-value=\"{{config_values if config_values else keys_value}}\"\n                                     startup-config-value=\"{{keys_value}}\"\n                                     data-type=\"text\"\n                                     data-pk=\"1\"\n                                     data-title=\"Enter api key\"\n                                     data-onblur=\"submit\"\n                                     data-highlight=true\n                                     class=\"editable editable-click config-element\">\n                        {{keys_value}}</a><br>\n\n                        API Secret : <a href=\"#\"\n                                        id=\"exchange_api-secret\"\n                                        config-key=\"exchanges_{{exchange}}_api-secret\"\n                                        config-type=\"global_config\"\n                                        config-value=\"{{config_values if config_values else keys_value}}\"\n                                        startup-config-value=\"{{keys_value}}\"\n                                        data-type=\"text\"\n                                        data-pk=\"1\"\n                                        data-title=\"Enter exchange api secret\"\n                                         data-onblur=\"submit\"\n                                         data-highlight=true\n                                        class=\"editable editable-click config-element\">\n                        {{keys_value}}</a><br>\n\n                        <i>API Pass/UID</i>\n                        <i class=\"fa-solid fa-question\"\n                           data-toggle=\"tooltip\" data-placement=\"top\"\n                           title=\"An API password or Memo/UID is required by some exchanges, leave as is otherwise.\">\n                        </i> : <a href=\"#\"\n                                  id=\"exchange_api-password\"\n                                  config-key=\"exchanges_{{exchange}}_api-password\"\n                                  config-type=\"global_config\"\n                                  config-value=\"{{config_values if config_values else keys_value}}\"\n                                  startup-config-value=\"{{keys_value}}\"\n                                  data-type=\"text\"\n                                  data-pk=\"1\"\n                                  data-title=\"Enter exchange api password\"\n                                  data-onblur=\"submit\"\n                                  data-highlight=true\n                                  class=\"editable editable-click config-element\">\n                        {{keys_value}}</a><br>\n                    </p>\n                </div>\n                {% endif %}\n                {% if not lite_config %}\n                <div class=\"\">\n                    <div\n                            class=\"custom-control custom-switch\"\n                            data-toggle=\"tooltip\"\n                            data-placement=\"top\"\n                            title=\"Disabled exchanges are not used by OctoBot.\">\n                        <input type=\"checkbox\"\n                               class=\"custom-control-input config-element\"\n                               id=\"exchange_{{exchange}}_enabled\"\n                               config-key=\"exchanges_{{exchange}}_enabled\"\n                               config-type=\"global_config\"\n                               config-value=\"{{enabled}}\"\n                               startup-config-value=\"{{enabled}}\"\n                               data-type=\"bool\"\n                               {{ 'checked' if enabled else '' }}>\n                        <label class=\"custom-control-label\" for=\"exchange_{{exchange}}_enabled\">Enabled</label>\n                    </div>\n                    {% if full_config %}\n                    <div\n                            class=\"custom-control custom-switch\"\n                            data-toggle=\"tooltip\"\n                            data-placement=\"top\"\n                            title=\"Enable the sandbox to use the exchange test website. Only works for exchanges supporting this feature.\">\n                        <input type=\"checkbox\"\n                               class=\"custom-control-input config-element\"\n                               id=\"exchange_{{exchange}}_sandboxed\"\n                               config-key=\"exchanges_{{exchange}}_sandboxed\"\n                               config-type=\"global_config\"\n                               config-value=\"{{sandboxed}}\"\n                               startup-config-value=\"{{sandboxed}}\"\n                               data-type=\"bool\"\n                               {{ 'checked' if sandboxed else '' }}>\n                        <label class=\"custom-control-label\" for=\"exchange_{{exchange}}_sandboxed\">Trade in exchange sandbox</label>\n                    </div>\n                    {% else %}\n                    <span\n                         config-key=\"exchanges_{{exchange}}_exchange-type\"\n                         config-type=\"global_config\"\n                         config-value=\"{{selected_exchange_type}}\"\n                         startup-config-value=\"{{selected_exchange_type}}\"\n                         current-value=\"{{selected_exchange_type}}\"\n                         data-type=\"text\"\n                         data-summary-field=\"radio-select\"\n                         class=\"row mx-auto config-element\"\n                    >\n                        {% for exchange_type in exchanges_details['supported_exchange_types'] %}\n                        <div class=\"col form-check\">\n                          <input class=\"form-check-input\" type=\"radio\"\n                                 name=\"exchangeTypeRadioOptions{{exchange}}\"\n                                 id=\"exchangeTypeRadio{{exchange}}{{exchange_type.value}}\"\n                                 value=\"{{exchange_type.value}}\"\n                                 {{'checked' if exchange_type.value == selected_exchange_type else ''}}>\n                          <label class=\"col form-check-label\"\n                                 for=\"exchangeTypeRadio{{exchange}}{{exchange_type.value}}\">{{exchange_type.value}}</label>\n                        </div>\n                        {% endfor %}\n                    </span>\n                    {% endif %}\n                </div>\n                {% endif %}\n            </div>\n            {% if full_config and not lite_config %}\n            <button type=\"button\" class=\"btn btn-danger remove-btn px-3 waves-effect\"><i class=\"fa fa-ban\" aria-hidden=\"true\"></i> Remove</button>\n            {% endif %}\n        </div>\n    </div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/notification_config.html",
    "content": "{% import 'components/config/editable_config.html' as m_editable_config %}\n{% macro config_notification(config, config_name, service_name_list) -%}\n    <!-- Card -->\n    <div class=\"card mb-4 config-card\" >\n\n    <div class=\"card-header\"><h4>Enabled notification events</h4></div>\n\n        <!--Card content-->\n        <div class=\"card-body\">\n\n            <p class=\"card-text config\">\n                {% for key in config %}\n                    {{ m_editable_config.editable_key(  config,\n                                                        key,\n                                                        config_name + \"_\" + key,\n                                                        \"global_config\",\n                                                        config[key],\n                                                        config[key],\n                                                        suggestions=service_name_list,\n                                                        placeholder_str=\"Select notification(s) to enable\")\n                    }}\n                {% endfor %}\n            </p>\n        </div>\n    </div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/profiles.html",
    "content": "{% macro profile_details(profile, tentacles_details, strategy_config, evaluator_config,\nget_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader,\nget_filtered_list, read_only=False) %}\n<div class=\"profile-details d-flex pb-1\" data-id=\"{{profile.profile_id}}\">\n    <div class=\"flex-grow-1\">\n        <h4>About\n            <span\n               id=\"profile-name-{{profile.profile_id}}\"\n               data-pk=\"1\"\n               data-type=\"text\"\n               data-clear=false\n               data-onblur=\"submit\"\n               data-highlight=true\n               data-emptytext=\"my profile\"\n               class=\"{{'editable profile-name-editor' if not profile.read_only}}\">{{ profile.name }}</span>\n            profile:\n        </h4>\n        <p>\n            <span class=\"large-editable\">\n                <span\n                   id=\"profile-description-{{profile.profile_id}}\"\n                   data-pk=\"1\"\n                   data-type=\"textarea\"\n                   data-clear=false\n                   data-onblur=\"submit\"\n                   data-highlight=true\n                   data-emptytext=\"my description\"\n                   data-inputclass=\"w-100\"\n                   class=\"{{'editable profile-description-editor' if not profile.read_only}}\">{{ profile.description }}</span>\n            </span>\n        </p>\n        <div class=\"d-flex justify-content-around\">\n            <div>\n                {{ profile_auto_update(profile, read_only or profile.read_only) }}\n            </div>\n            <div>\n                {{ profile_complexity(profile, read_only or profile.read_only) }}\n            </div>\n            <div>\n                {{ profile_risk(profile, read_only or profile.read_only) }}\n            </div>\n        </div>\n    </div>\n    <div>\n        {% if not (read_only or profile.read_only) %}\n        <button id=\"save-profile-{{profile.profile_id}}\"\n                data-url=\"{{url_for('profiles_management', action='update')}}\"\n                class=\"btn btn-success rounded-circle waves-effect px-3 save-profile\"\n                role=\"button\" data-toggle=\"tooltip\" title=\"Save\" disabled>\n            <i class=\"fas fa-save\" aria-hidden=\"true\"></i>\n        </button>\n        {% endif %}\n    </div>\n</div>\n<div>\n    <h4>\n        Overview:\n        <span class=\"float-right\"><span class=\"d-none d-md-inline\">Built on OctoBot </span><span class=\"badge badge-light\">{{tentacles_details[\"version\"]}}</span></span>\n    </h4>\n    <div>\n        <div class=\"row\">\n            <div class=\"card profile-card my-auto my-lg-2 px-0 mx-auto col-12 col-lg-5\">\n                <div class=\"card-header\">\n                    <h5>Traded pairs</h5>\n                </div>\n                <div class=\"card-body py-0 d-flex flex-wrap\">\n                    {% for currency, pairs in get_profile_traded_pairs_by_currency(profile).items() %}\n                    <div class=\"row w-100 p-1\">\n                        <div class=\"my-auto col-3\">\n                            <div class=\"animated fadeIn img-fluid very-small-size\">\n                                <img class=\"card-img-top currency-image\"\n                                     src=\"{{ url_for('static', filename='img/svg/loading_currency.svg') }}\"\n                                     alt=\"{{ currency }}\"\n                                     data-name=\"{{currency.lower()}}\">\n                            </div>\n                        </div>\n                        <div class=\"col-9 my-auto profile-overview-values px-2\">\n                            {{pairs | join(', ')}}\n                        </div>\n                    </div>\n                    {% endfor %}\n                </div>\n            </div>\n            <div class=\"card profile-card my-1 my-lg-2 px-0 mx-auto col-12 col-lg-5\">\n                <div class=\"card-header\">\n                    <h5>\n                        Exchanges\n                        {% if get_enabled_trader(profile) %}\n                        <span class=\"float-right badge badge-info\">\n                            {{ get_enabled_trader(profile)}}\n                        </span>\n                        {% endif %}\n                    </h5>\n                </div>\n                <div class=\"card-body py-0 d-flex flex-wrap\">\n                    {% for exchange in get_profile_exchanges(profile) %}\n                    <div class=\"my-auto p-1 p-md-3\">\n                        <a href=\"\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"waves-effect\">\n                            <img class=\"img-fluid product-logo\" src=\"\" alt=\"{{exchange}}\" url=\"{{url_for('exchange_logo', name=exchange)}}\">\n                        </a>\n                    </div>\n                    {% endfor %}\n                </div>\n            </div>\n            <div class=\"card profile-card my-1 my-lg-2 px-0 mx-auto col-12 col-lg-11\">\n                <div class=\"card-header\">\n                    <h5>\n                        Tentacles configuration\n                    </h5>\n                </div>\n                <div class=\"card-body py-0 px-2 px-lg-4\">\n                    {% set trading_modes = get_filtered_list(tentacles_details[\"activation\"], strategy_config[\"trading-modes\"]) %}\n                    {% if trading_modes %}\n                    <div class=\"my-1 my-md-3\">\n                        <span class=\"profile-overview-explanation\">Use </span>\n                        <span class=\"profile-overview-values\">\n                        {% for tentacle in trading_modes %}\n                            <a href=\"#\" class=\"profile-overview-values hover_anim\"\n                               data-toggle=\"modal\" data-target=\"#{{tentacle}}Modal\">{{tentacle}}</a>\n                        {% endfor %}\n                        </span>\n                        <span class=\"profile-overview-explanation\">as {{'a' if trading_modes | length == 1}} trading mode{{'s' if trading_modes | length > 1 }}</span>.\n                    </div>\n                    {% endif %}\n                    {% set strategies = get_filtered_list(tentacles_details[\"activation\"], strategy_config[\"strategies\"]) %}\n                    {% if strategies %}\n                    <div class=\"my-1 my-md-3\">\n                        <span class=\"profile-overview-explanation\">With </span>\n                        <span class=\"profile-overview-values\">\n                        {% for tentacle in strategies %}\n                            <a href=\"#\" class=\"profile-overview-values hover_anim\"\n                               data-toggle=\"modal\" data-target=\"#{{tentacle}}Modal\">{{tentacle}}</a>\n                        {% endfor %}\n                        </span>\n                        <span class=\"profile-overview-explanation\">as {{'a' if strategies | length == 1}} strateg{{'ies' if strategies | length > 1 else 'y'}} and</span>\n                        <ul>\n                            {% set TAs = get_filtered_list(tentacles_details[\"activation\"], evaluator_config[\"ta\"]) %}\n                            {% if TAs %}\n                            <li>\n                                <span class=\"profile-overview-values\">\n                                    {% for tentacle in TAs %}\n                                        <a href=\"#\" class=\"profile-overview-values hover_anim\"\n                                           data-toggle=\"modal\" data-target=\"#{{tentacle}}Modal\">{{tentacle}}</a>\n                                    {% endfor %}\n                                </span>\n                                <span class=\"profile-overview-explanation\">as {{'a' if TAs | length == 1}} technical evaluator{{'s' if TAs | length > 1}}.</span>\n                            </li>\n                            {% endif %}\n                            {% set socials = get_filtered_list(tentacles_details[\"activation\"], evaluator_config[\"social\"]) %}\n                            {% if socials %}\n                            <li>\n                                <span class=\"profile-overview-values\">\n                                    {% for tentacle in socials %}\n                                        <a href=\"#\" class=\"profile-overview-values hover_anim\"\n                                           data-toggle=\"modal\" data-target=\"#{{tentacle}}Modal\">{{tentacle}}</a>\n                                    {% endfor %}\n                                </span>\n                                <span class=\"profile-overview-explanation\">as {{'a' if socials | length == 1}} social evaluator{{'s' if socials | length > 1}}.</span>\n                            </li>\n                            {% endif %}\n                            {% set RTs = get_filtered_list(tentacles_details[\"activation\"], evaluator_config[\"real-time\"]) %}\n                            {% if RTs %}\n                            <li>\n                                <span class=\"profile-overview-values\">\n                                    {% for tentacle in RTs %}\n                                        <a href=\"#\" class=\"profile-overview-values hover_anim\"\n                                           data-toggle=\"modal\" data-target=\"#{{tentacle}}Modal\">{{tentacle}}</a>\n                                    {% endfor %}\n                                </span>\n                                <span class=\"profile-overview-explanation\">as {{'a' if RTs | length == 1}} real-time evaluator{{'s' if RTs | length > 1}}.</span>\n                            </li>\n                            {% endif %}\n                        </ul>\n                    </div>\n                    {% endif %}\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n{% endmacro %}\n\n{% macro profile_auto_update(profile, read_only) %}\n<div class=\"{{'d-flex' if not read_only}}\">\n    <label class=\"my-auto\" for=\"{{profile.profile_id}}profile-auto-update\">\n      Auto-update:\n    </label>\n    <span class=\"badge {{'text-light bg-rating-1' if profile.auto_update else 'badge-primary'}} my-auto\">\n        {{'Enabled' if profile.auto_update else 'Disabled'}}\n    </span>\n</div>\n{% endmacro %}\n\n{% macro profile_complexity(profile, read_only) %}\n<div class=\"{{'d-flex' if not read_only}}\">\n    <label class=\"my-auto\" for=\"{{profile.profile_id}}profile-complexity\">\n      Complexity:\n    </label>\n    {% if read_only %}\n    <span class=\"badge text-light bg-rating-{{profile.complexity.value}}\">\n        {{profile.complexity.name | capitalize}}\n    </span>\n    {% else %}\n    <select id=\"{{profile.profile_id}}-profile-complexity\" class=\"form-control profile-complexity-selector\">\n        <option value=\"1\" {{'selected' if profile.complexity.value == 1}}>\n            Easy\n        </option>\n        <option value=\"2\" {{'selected' if profile.complexity.value == 2}}>\n            Medium\n        </option>\n        <option value=\"3\" {{'selected' if profile.complexity.value == 3}}>\n            Difficult\n        </option>\n    </select>\n    {% endif %}\n</div>\n{% endmacro %}\n\n{% macro profile_risk(profile, read_only) %}\n<div class=\"{{'d-flex' if not read_only}}\">\n    <label class=\"my-auto\" for=\"{{profile.profile_id}}profile-risk\">\n      Risk:\n    </label>\n    {% if read_only %}\n    <span class=\"badge text-light bg-rating-{{profile.risk.value}}\">\n        {{profile.risk.name | capitalize}}\n    </span>\n    {% else %}\n    <select id=\"{{profile.profile_id}}-profile-risk\" class=\"form-control profile-risk-selector\">\n        <option value=\"1\" {{'selected' if profile.risk.value == 1}}>\n            Low\n        </option>\n        <option value=\"2\" {{'selected' if profile.risk.value == 2}}>\n            Moderate\n        </option>\n        <option value=\"3\" {{'selected' if profile.risk.value == 3}}>\n            High\n        </option>\n    </select>\n    {% endif %}\n</div>\n{% endmacro %}\n\n{% macro profile_tab(current_profile, profile, tentacles_details, strategy_config, evaluator_config, get_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader, get_filtered_list, OCTOBOT_DOCS_URL) %}\n<div class=\"tab-pane fade\" id=\"panelProfile{{profile.profile_id}}\" role=\"tabpanel\"\n     aria-labelledby=\"panelExchanges-tab\">\n    <div class=\"card\">\n        <div class=\"card-header\">\n            <h2 class=\"card-title\" id=\"#profileModalLabel\">\n                <span class=\"d-none d-md-inline\">Profile: </span><span data-role=\"profile-name\" data-profile-id=\"{{profile.profile_id}}\"\n                               class=\"text-bold\">{{profile.name}}</span>\n                <button class=\"btn btn-sm rounded-circle btn-primary waves-effect activate-profile-button align-middle\"\n                        data-url=\"{{url_for('profile', select=profile.profile_id)}}\"\n                        {{ 'disabled' if current_profile.profile_id == profile.profile_id else '' }}\n                        data-toggle=\"tooltip\" title=\"Use this profile\">\n                    <i class=\"fas fa-check\" aria-hidden=\"true\"></i>\n                </button>\n                <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-configuration/profiles?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=profiles_config\">\n                    &nbsp <i class=\"fa-solid fa-question\"></i>\n                </a>\n            </h2>\n        </div>\n        <div class=\"card-body deck-container\">\n            {{ profile_details(profile, tentacles_details, strategy_config, evaluator_config, get_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader, get_filtered_list) }}\n        </div>\n        <div class=\"card-footer d-flex justify-content-between\">\n            <div>\n                <button id=\"export-profile\"\n                        data-url=\"{{url_for('profiles_management', action='export', profile_id=profile.profile_id)}}\"\n                        class=\"btn btn-outline-primary waves-effect export-profile-button\"\n                        data-toggle=\"tooltip\" title=\"Share this profile\">\n                    <i class=\"fas fa-share-square\"></i> <span class=\"d-none d-md-inline\">Share</span>\n                </button>\n            </div>\n\n            <button class=\"btn btn{{ '-outline' if current_profile.profile_id == profile.profile_id else '' }}-primary waves-effect activate-profile-button\"\n                    data-url=\"{{url_for('profile', select=profile.profile_id)}}\"\n                    {{ 'disabled' if current_profile.profile_id == profile.profile_id else '' }}>\n                <i class=\"fas fa-check\" aria-hidden=\"true\"></i> <span class=\"d-none d-md-inline\">\n                    {{ 'This is your current profile' if current_profile.profile_id == profile.profile_id else 'Use this profile' }}\n                </span>\n            </button>\n            <div class=\"d-flex\">\n                <button id=\"duplicate-profile\"\n                        data-url=\"{{url_for('profiles_management', action='duplicate', profile_id=profile.profile_id)}}\"\n                        class=\"btn btn-primary waves-effect duplicate-profile px-3\">\n                    <i class=\"fas fa-copy\" aria-hidden=\"true\"></i> <span class=\"d-none d-md-inline\">Duplicate</span>\n                </button>\n                {% if current_profile.profile_id != profile.profile_id and (profile.imported or not profile.read_only) %}\n                <button class=\"btn btn-outline-danger ml-2 px-3 waves-effect remove-profile-button\"\n                        id=\"removeProfile{{profile.profile_id}}\"\n                        data-profile-id=\"{{profile.profile_id}}\"\n                        data-url=\"{{url_for('profiles_management', action='remove')}}\">\n                    <i class=\"fa fa-ban\" aria-hidden=\"true\"></i> <span class=\"d-none d-md-inline\">Remove</span>\n                </button>\n                {% endif %}\n            </div>\n        </div>\n        <div class=\"card-footer\">\n            {% if profile.auto_update %}\n                <p>This profile is kept up-to-date from its OctoBot cloud equivalent. When an update occurs, your OctoBot might automatically restart to apply the new version of this profile. Duplicate it to disable this behavior.</p>\n            {% endif %}\n            {% if profile.read_only %}\n                <p><i class=\"fa fa-info\"></i> This profile is ready only, press duplicate to create a copy to be able to edit it.</p>\n            {% endif %}\n        </div>\n    </div>\n    <div class=\"text-right mt-4\">\n        <button class=\"btn btn-outline-primary waves-effect\"\n                data-toggle=\"modal\" data-target=\"#importProfileModal\">\n            Import a profile\n        </button>\n    </div>\n</div>\n{% endmacro %}\n\n\n{% macro profile_import_modal(next=None) %}\n<div class=\"modal fade\" id=\"importProfileModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#importProfileModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog modal-dialog-centered modal-lg\" role=\"document\">\n    <div class=\"modal-content modal-text\">\n      <div class=\"modal-header primary-text\">\n        <h5 class=\"modal-title\" id=\"#importProfileModalLabel\">Import a new profile</h5>\n        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n      </div>\n      <div class=\"modal-body text-center\">\n          <div>\n              <form class=\"form-inline profile-download-form\"\n                    action=\"{{ url_for('profiles_management', action='download', next=next) }}\" method=\"POST\">\n                <div class=\"form-group mx-sm-3 mb-2 w-75\">\n                  <label for=\"inputProfileLink\" class=\"mr-2\">Import from a profile link</label>\n                  <input type=\"text\" class=\"form-control w-100 w-md-50\" id=\"inputProfileLink\" name=\"inputProfileLink\"\n                         placeholder=\"https://profile-to-import-url.zip\">\n                </div>\n                <button type=\"button\" class=\"btn btn-primary waves-effect mb-2\"\n                        data-role=\"download-profile-button\">Import</button>\n              </form>\n          </div>\n      </div>\n      <div class=\"modal-footer\">\n        <div>\n          <form class=\"d-none profile-import-form\" action=\"{{ url_for('profiles_management', action='import', next=next) }}\" method=\"POST\"\n           enctype = \"multipart/form-data\">\n              <input class=\"profile-input\" type=\"file\" name=\"file\" accept=\".zip\"/>\n          </form>\n          <button class=\"btn btn-outline-primary waves-effect import-profile-button\">\n            <i class=\"fas fa-download\"></i> Import a local profile\n          </button>\n        </div>\n        <button type=\"button\" class=\"btn btn-primary\" data-dismiss=\"modal\">Close</button>\n      </div>\n    </div>\n  </div>\n</div>\n{% endmacro %}\n\n{% macro profile_overview(profile, current_profile, tentacles_details, strategy_config, evaluator_config,\n                          get_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader,\n                          get_filtered_list, read_only=False, reboot=False, onboarding=False) -%}\n<div class=\"col-12 col-md-6 col-lg-4 px-1 profile-overview {{'profile-overview-selected' if profile.profile_id == current_profile.profile_id}}\">\n    <div class=\"p-2 vertically-aligned\">\n        <div class=\"row mx-0\">\n            <div class=\"col-9\">\n                <h4>{{ profile.name | safe }}</h4>\n            </div>\n            <div class=\"col-1 col-md-2 text-right\">\n                {% if profile.profile_id == current_profile.profile_id %}\n                <h4>\n                    <span class=\"badge badge-danger float-right d-none d-md-block\">active</span>\n                    <i class=\"fas fa-check d-md-none\"\n                        data-toggle=\"tooltip\" data-placement=\"top\"\n                        title=\"Active\"\n                    ></i>\n                </h4>\n                {% endif %}\n            </div>\n            <div class=\"col-1 text-right\">\n                <h4>\n                {% if profile.imported %}\n                    <i class=\"fa-sharp fa-solid fa-cloud-arrow-down\"\n                        data-toggle=\"tooltip\" data-placement=\"top\"\n                        title=\"Downloaded\"></i>\n                {% elif profile.read_only %}\n                    <i class=\"fa-brands fa-octopus-deploy\"\n                        data-toggle=\"tooltip\" data-placement=\"top\"\n                        title=\"Pre-installed\"></i>\n                {% else %}\n                    <i class=\"fa-solid fa-user\"\n                        data-toggle=\"tooltip\" data-placement=\"top\"\n                        title=\"Custom\"></i>\n                {% endif %}\n                </h4>\n            </div>\n        </div>\n        </h4>\n        <div class=\"text-center\">\n            <img src=\"{{url_for('profile_media', path=profile.avatar_path) if profile.avatar_path else url_for('static', filename='img/default_profile.png')}}\"\n                 class=\"img-fluid profile-overview-image\" alt=\"{{profile.name}}\">\n        </div>\n        <p class=\"pt-2 text-center\">\n            {{ profile.description | safe }}\n        </p>\n        <div class=\"d-flex justify-content-between px-2 px-md-4\">\n            <div>\n                <button class=\"btn btn-outline-primary btn-md\"\n                        data-toggle=\"modal\" data-target=\"#{{profile.profile_id}}Modal\">\n                    <i class=\"fas fa-ellipsis\"></i>\n                </button>\n                {{ profile_details_modal(profile, tentacles_details, strategy_config, evaluator_config,\n                                         get_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader,\n                                         get_filtered_list, read_only=read_only) }}\n            </div>\n            <div>\n                {% if profile.profile_id != current_profile.profile_id %}\n                <button class=\"btn btn-primary btn-md activate-profile-button\"\n                        data-toggle=\"tooltip\" data-placement=\"top\"\n                        title=\"Select this profile\"\n                        data-url=\"{{url_for('profile', select=profile.profile_id,\n                                    next=url_for('trading_type_selector', reboot=reboot, onboarding=onboarding))}}\">\n                    <i class=\"fas fa-check\"></i>\n                </button>\n                {% else %}\n                <button class=\"btn btn-outline-primary btn-md activate-profile-button\"\n                        data-toggle=\"tooltip\" data-placement=\"top\"\n                        title=\"Proceed with this profile\"\n                        data-url=\"{{url_for('profile', select=profile.profile_id,\n                                    next=url_for('trading_type_selector', reboot=False, onboarding=onboarding))}}\">\n                    <i class=\"fas fa-arrow-right\"></i>\n                </button>\n                {% endif %}\n            </div>\n        </div>\n    </div>\n</div>\n{%- endmacro %}\n\n{% macro profile_details_modal(profile, tentacles_details, strategy_config, evaluator_config,\nget_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader,\nget_filtered_list, read_only=False) %}\n<div class=\"modal fade\" id=\"{{profile.profile_id}}Modal\" tabindex=\"-1\" role=\"dialog\"\n     aria-labelledby=\"#{{profile.profile_id}}ModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog modal-dialog-centered modal-lg\" role=\"document\">\n    <div class=\"modal-content modal-text mt-4\">\n      <div class=\"modal-header primary-text\">\n          <h5 class=\"modal-title\" id=\"{{profile.profile_id}}ModalLabel\">\n              {{profile.name}}\n          </h5>\n        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n      </div>\n      <div class=\"modal-body\">\n        {{ profile_details(profile, tentacles_details, strategy_config,\n          evaluator_config, get_profile_traded_pairs_by_currency, get_profile_exchanges,\n          get_enabled_trader, get_filtered_list, read_only=read_only) }}\n      </div>\n    </div>\n  </div>\n</div>\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/service_card.html",
    "content": "{% import 'components/config/editable_config.html' as m_editable_config %}\n{% macro config_service_card(config, service_name, service, add_class='', no_select=False, default_values=False, extension_name='') -%}\n    {% set service_config_fields = service.get_default_value() %}\n\n    <!-- Card -->\n    <div class=\"card mb-4 {{add_class}} config-card\">\n\n        <div class=\"card-header d-flex\">\n            <div class=\"d-flex justify-content-between w-100 px-3\">\n                <div>\n                    <h4 class=\"text-capitalize\">\n                        {{service_name}}\n                    </h4>\n                </div>\n                {% if service.get_website_url() %}\n                    <div>\n                        <a href=\"{{ service.get_website_url() }}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"waves-effect\">\n                            <img class=\"img-fluid brand-logo\" alt=\"{{service_name}}-logo\" src=\"{{ service.get_logo() }}\">\n                        </a>\n                    </div>\n                {% endif %}\n                <div>\n                    <h4>\n                        <a class=\"blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{service.get_help_page()}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=service_config\">\n                            <i class=\"fa-solid fa-question\"></i>\n                        </a>\n                    </h4>\n                </div>\n            </div>\n        </div>\n\n        <!--Card content-->\n        <div class=\"card-body\" name=\"{{service_name}}\" config-key=\"services_{{service_name}}\">\n\n            <p class=\"card-text api analytics-hidden\">\n                {% for req in service_config_fields %}\n                    {{ m_editable_config.editable_key(  service_config_fields if (default_values or req not in config[service_name]) else config[service_name],\n                                                        req,\n                                                        \"services_\" + service_name + \"_\" + req,\n                                                        \"global_config\",\n                                                        service_config_fields[req] if default_values else config[service_name][req],\n                                                        service_config_fields[req] if default_values else config[service_name][req],\n                                                        suggestions=service_config_fields[req] if default_values else config[service_name][req],\n                                                        no_select=no_select,\n                                                        number_step=1,\n                                                        force_title=True,\n                                                        tooltip=service.get_fields_description()[req],\n                                                        identifier=service_name,\n                                                        placeholder_str=\"Add user(s) in whitelist\")\n                    }}\n                {% endfor %}\n            </p>\n            <p>\n                {% if service.is_improved_by_extensions() %}\n                    <i class=\"fa fa-info-circle\"></i> The {{service_name}} interface is improved by the <a href=\"{{ url_for('extensions') }}\">{{extension_name}}</a>.\n                {% endif %}\n            </p>\n            <button type=\"button\" class=\"btn btn-danger remove-btn px-3 waves-effect\"><i class=\"fa fa-ban\" aria-hidden=\"true\"></i> Remove</button>\n            <div class=\"float-right card-text mt-2\">\n                {% for read_only_info in service.get_read_only_info() %}\n                <div class=\"\">\n                    <i class=\"fa fa-check mr-1\"></i>\n                    <span class=\"mr-2\">\n                        {{ read_only_info.name }}\n                    </span>\n                    {% if read_only_info.type.value == \"clickable\" %}\n                        <a class=\"text-danger external-link\" target=\"_blank\" rel=\"noopener noreferrer\" href=\"{{url_for(read_only_info.path) if read_only_info.path else read_only_info.value}}\">\n                            {{ read_only_info.value }}\n                        </a>\n                    {% elif read_only_info.type.value == \"copyable\" %}\n                        <span\n                            class=\"font-weight-bolder pointer-cursor\"\n                            data-role=\"copy-to-clipboard\" data-name=\"{{read_only_info.name}}\" data-value=\"{{read_only_info.value}}\"\n                            data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"Click to copy\"\n                        >\n                            <i class=\"fa fa-copy mr-1\"></i>\n                            {{ read_only_info.value }}\n                        </span>\n                    {% elif read_only_info.type.value == \"cta\" %}\n                        <a\n                            type=\"button\" class=\"btn btn-sm btn-primary\"\n                            href=\"{{url_for(read_only_info.path) if read_only_info.path else read_only_info.value}}\"\n                        >\n                            <i class=\"fa fa-plus mr-1\"></i>\n                            {{ read_only_info.value }}\n                        </a>\n                    {% else %}\n                        {{ read_only_info.value }}\n                    {% endif %}\n                    {% if read_only_info.configuration_path %}\n                        <a\n                            href=\"{{url_for(read_only_info.configuration_path)}}\"\n                            data-toggle=\"tooltip\" data-placement=\"bottom\"\n                            title=\"{{read_only_info.configuration_title or 'Configure'}}\"\n                        >\n                            <i class=\"fa fa-cog ml-1\"></i>\n                        </a>\n                    {% endif %}\n                </div>\n                {% endfor %}\n            </div>\n\n        </div>\n    </div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/tentacle_card.html",
    "content": "{% import 'macros/tentacles.html' as m_tentacles %}\n\n{% macro config_tentacle_card(name, info, can_be_disabled) %}\n    <a href=\"#\" onclick=\"return false;\"\n       class=\"col-md-6 col-lg-4 p-1 rounded list-group-item waves-effect {{'list-group-item-success' if info['activation'] else 'list-group-item-light'}} config-element\"\n       id=\"{{ name }}\" name=\"{{ name }}\"\n       config-type=\"tentacle_config\" config-key={{name}} current-value={{info['activation']}} config-value={{info['activation']}} startup-config-value={{info['startup_config']}}\n       {{ \"no-activation-click='true'\" if not can_be_disabled}}\n    >\n        <span class=\"ps-2\">\n            {{ name }}\n        </span>\n        <span class=\"float-right\">\n            <span class=\"badge {{'badge-warning' if info['startup_config'] != info['activation'] else ('badge-success' if info['activation'] else 'badge-secondary')}}\">\n                {{('Activation pending restart' if info['activation'] else 'Deactivation pending restart') if info['startup_config'] != info['activation'] else ('Activated' if info['activation'] else 'Deactivated')}}\n            </span>\n            <button class=\"btn btn-outline-primary btn-md {{'waves-effect' if info['description'] else 'disabled'}}\"  data-toggle=\"modal\" data-target=\"#{{name}}Modal\" no-activation-click=\"true\"><i class=\"fa fa-info-circle\" no-activation-click=\"true\"></i></button>\n        </span>\n    </a>\n    {% if info['description'] %}\n        {{ tentacle_card_modal(name, info) }}\n    {% endif %}\n{% endmacro %}\n\n{% macro tentacle_card_modal(name, info) %}\n    <div class=\"modal fade\" id=\"{{name}}Modal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#{{name}}ModalLabel\" aria-hidden=\"true\">\n      <div class=\"modal-dialog modal-dialog-centered modal-lg\" role=\"document\">\n        <div class=\"modal-content modal-text mt-4\">\n          <div class=\"modal-header primary-text\">\n              <h5 class=\"modal-title\" id=\"#{{name}}ModalLabel\">\n                  {{name}}\n              </h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\">\n              {{ m_tentacles.tentacle_description(info, strategy, name ) }}\n          </div>\n          <div class=\"modal-footer\">\n            <a href=\"{{ url_for('config_tentacle', name=name) }}\"\n               class=\"btn btn-primary waves-effect\" role=\"button\">Configure</a>\n            <button class=\"btn btn-primary waves-effect\" data-dismiss=\"modal\">Close</button>\n          </div>\n        </div>\n      </div>\n    </div>\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/tentacle_config_editor.html",
    "content": "{% macro tentacles_config_editor(name, class_name) %}\n    <div class=\"{{class_name}}\" id=\"configEditorBody\"\n         data-edit-details-url=\"{{ url_for('config_tentacle_edit_details', tentacle=name) }}\">\n        <div id=\"configEditor\"></div>\n        <div id=\"editor-waiter\" class=\"text-center my-4\">\n            <div>\n                <h2>Loading configuration</h2>\n            </div>\n            <div>\n                <h2><i class=\"fa fa-spinner fa-spin\"></i></h2>\n            </div>\n        </div>\n        <div id=\"noConfigMessage\" style='display: none;'>\n            This {{ tentacle_type }} can't be configured.\n        </div>\n        <div id=\"configErrorDetails\" style='display: none;'>\n            <div>\n                Error when fetching tentacle config. Resetting its configuration should fix the issue.\n            </div>\n            <div>\n                <button class=\"btn btn-warning waves-effect\" data-role='factoryResetConfig'\n                        update-url=\"{{ url_for('config_tentacle', name=name, action='factory_reset') }}\"><i class=\"fas fa-recycle\"></i> Reset configuration to default values</button>\n            </div>\n        </div>\n    </div>\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/config/trader_card.html",
    "content": "{% import 'components/config/editable_config.html' as m_editable_config %}\n{% macro config_trader_card(config, config_name, trader, add_class='', link='', action=None, footer_text=None) -%}\n    <!-- Card -->\n    <div class=\"card mb-4 {{add_class}} config-card\">\n\n        <div class=\"card-header\">\n            <h4>\n                {{trader}}\n                <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{link}}\">\n                    <i class=\"fa-solid fa-question\"></i>\n                </a>\n            </h4>\n        </div>\n\n        <!--Card content-->\n        <div class=\"card-body\">\n\n            <p class=\"card-text config\">\n                {% for key in config %}\n                    {{ m_editable_config.editable_key(  config,\n                                                        key,\n                                                        config_name + \"_\" + key,\n                                                        \"global_config\",\n                                                        config[key],\n                                                        config[key],\n                                                        number_step=0.001,\n                                                        allow_create_for=\"starting-portfolio\")\n                    }}\n                {% endfor %}\n                {% if action %}\n                    <div class=\"text-center\">\n                        <button class=\"btn btn-warning waves-effect\" type=\"button\" action=\"post\" update-url=\"{{ action[1] }}\">{{ action[0] }}</button>\n                    </div>\n                {% endif %}\n                {% if footer_text %}\n                    <div>\n                        <i class=\"fa-solid fa-circle-info\"></i> {{ footer_text }}\n                    </div>\n                {% endif %}\n            </p>\n        </div>\n    </div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/modals/generic_modal.html",
    "content": "{% macro create_generic_modal() -%}\n    <div class=\"modal fade\" id=\"genericModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"genericModalTitle\" aria-hidden=\"true\">\n      <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n        <div class=\"modal-content\">\n          <div class=\"modal-header\">\n            <h5 class=\"modal-title\" id=\"genericModalTitle\"></h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\">\n            <div id=\"genericModalContent\">\n            </div>\n            <div class=\"alert alert-warning w-100 d-none\" id=\"genericModalWarning\" role=\"alert\">\n                    <strong>Warning!</strong>\n                    <div id=\"genericModalWarningMessage\"></div>\n            </div>\n          </div>\n          <div class=\"modal-footer text-center\">\n              <button class=\"my-2 btn-primary waves-effect\" id=\"genericModalButtonYes\">Yes</button>\n              <button class=\"my-2 btn-outline-primary waves-effect\" id=\"genericModalButtonNo\">No</button>\n          </div>\n        </div>\n      </div>\n    </div>\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/modals/trading_state_modal.html",
    "content": "{% import 'macros/trading_state.html' as m_trading_state %}\n{% macro create_trading_state_modal(is_real_trading, enabled_trader) -%}\n    <div class=\"modal fade\" id=\"tradingSwitchModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#tradingSwitchModalTitle\" aria-hidden=\"true\">\n      <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n        <div class=\"modal-content\">\n          <div class=\"modal-header\">\n            <h5 class=\"modal-title\" id=\"tradingSwitchModalTitle\">Currently using : {{ m_trading_state.display_trading_state(is_real_trading, enabled_trader, False, True) }}</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\">\n                {% if is_real_trading %}\n                    <p>By switching to simulated trading, OctoBot will only use its simulation mode on real market conditions.</p>\n                    <p>It will no longer create trade with your exchange account, it will use a simulated portfolio managed by OctoBot.</p>\n                {% else %}\n                    <p>By switching to real trading, OctoBot will use your <b>real</b> funds</p>\n                {% endif %}\n                <div class=\"alert alert-warning w-100\" role=\"alert\">\n                    <strong>Warning!</strong> The switch button will also restart OctoBot\n                </div>\n          </div>\n          <div class=\"modal-footer text-center\">\n            <a class=\"my-2 btn btn-outline-primary waves-effect\" href=\"\" data-dismiss=\"modal\" aria-label=\"Close\">Stay</a>\n              {% if is_real_trading %}\n                <a class=\"my-2 btn btn-primary waves-effect trading-mode-switch-button\" href=\"#\" update-url=\"/config\" href=\"/\" config-type=\"global_config\"\n                    config-key=\"trader-simulator_enabled\" current-value=\"True\">Switch to simulated trading</a>\n              {% else %}\n                <a class=\"my-2 btn btn-danger waves-effect trading-mode-switch-button\" href=\"#\" update-url=\"/config\" config-type=\"global_config\" config-key=\"trader_enabled\"\n                    current-value=\"True\">Switch to real trading</a>\n              {% endif %}\n          </div>\n        </div>\n      </div>\n    </div>\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/components/tentacles_packages/tentacles_package_card.html",
    "content": "{% macro tentacles_package_card(tentacles_package, default_image) -%}\n<div class=\"card tentacles-package-card m-4\">\n\n    <div class=\"card-header\">\n        <h2>\n            {{tentacles_package.name | replace(\"_\", \" \") | capitalize}}\n            {% if tentacles_package.activated %}\n                <span class=\"float-right badge badge-primary waves-effect\">\n                    Activated\n                </span>\n            {% endif %}\n        </h2>\n    </div>\n    <div class=\"card-body\">\n        <a href=\"{{tentacles_package.url}}\"\n           class=\"btn btn-outline-primary waves-effect\">\n            <img src=\"{{tentacles_package.images[0] if tentacles_package.images else default_image}}\" class=\"img-fluid\" alt=\"Tentacles package illustration\">\n            <p class=\"mt-2\">\n                See on OctoBot community\n            </p>\n        </a>\n    </div>\n</div>\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/config_tentacle.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n{% set page_title = name %}\n\n{% import 'macros/tentacles.html' as m_tentacles %}\n{% import 'macros/backtesting_utils.html' as m_backtesting_utils %}\n{% import 'components/config/tentacle_config_editor.html' as m_tentacle_config_editor %}\n{% import 'components/config/evaluator_card.html' as m_config_evaluator_card %}\n\n{% block body %}\n<br>\n{% if tentacle_desc %}\n    <div class=\"card\">\n        <div class=\"card-header\">\n            <h2 id='{{ name }}' config-type=\"evaluator_config\" default-elements=\"{{tentacle_desc['default-config']}}\">\n                {{ name }}\n                <a href='{{ url_for(\"profile\") if tentacle_type in [\"trading mode\", \"strategy\"] else url_for(\"advanced.evaluator_config\") }}'>\n                    {% if tentacle_desc[\"activation\"] %}\n                        <span class=\"badge badge-primary float-right waves-effect\">Activated</span>\n                    {% else %}\n                        <span class=\"badge badge-warning float-right waves-effect\">Deactivated</span>\n                    {% endif %}\n                </a>\n            </h2>\n        </div>\n        <div class=\"card-body\" id='defaultConfigDiv' update-url=\"{{ url_for('config') }}\">\n          {{ m_tentacles.tentacle_horizontal_description(tentacle_desc, tentacle_type==\"strategy\") }}\n        </div>\n    </div>\n    <br>\n    <div class=\"card\">\n        <div class=\"card-header\">\n            <h2>Configuration\n                <button data-role='saveConfig'\n                        class=\"btn btn-success rounded-circle waves-effect px-3 d-none\"\n                        update-url=\"{{ url_for('config_tentacle', name=name, action='update') }}\"\n                        role=\"button\" data-toggle=\"tooltip\" title=\"Save\">\n                    <i class=\"fas fa-save\" aria-hidden=\"true\"></i>\n                </button>\n                <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-configuration/profile-configuration#specific-evaluator-configuration\">\n                    <i class=\"fa-solid fa-question\"></i>\n                </a>\n            </h2>\n        </div>\n        {{ m_tentacle_config_editor.tentacles_config_editor(name, \"card-body\") }}\n        <div class=\"card-footer\" id='saveConfigFooter' style='display: none;'>\n            <button class=\"btn btn-primary waves-effect\" data-role='saveConfig' update-url=\"{{ url_for('config_tentacle', name=name, action='update', restart='false') }}\"><i class=\"fas fa-save\"></i> Save configuration and restart later</button>\n            <button class=\"btn btn-outline-primary waves-effect mx-5\" data-role='saveConfig' update-url=\"{{ url_for('config_tentacle', name=name, action='update', restart='true') }}\">Save and restart</button>\n            <button class=\"btn btn-outline-warning waves-effect float-right\" data-role='factoryResetConfig'\n                    update-url=\"{{ url_for('config_tentacle', name=name, action='factory_reset') }}\"><i class=\"fas fa-recycle\"></i> Reset configuration to default values</button>\n        </div>\n    </div>\n    {% if user_commands %}\n    <div class=\"card mt-2\">\n        <div class=\"card-header\">\n            <h2>Commands</h2>\n        </div>\n        <div class=\"card-body\">\n            {% for command_action, command_params in user_commands.items() %}\n                <!-- Button trigger modal -->\n                <button type=\"button\" class=\"btn btn-outline-primary waves-effect\"\n                        data-toggle=\"modal\" data-target=\"#{{ command_action | replace (' ', '') }}Modal\">\n                    {{ command_action }}\n                </button>\n\n                <!-- Modal -->\n                <div class=\"modal text-dark\" id=\"{{ command_action | replace (' ', '') }}Modal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"{{ command_action | replace (' ', '') }}ModalLabel\" aria-hidden=\"true\">\n                  <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n                    <div class=\"modal-content p-2\">\n                      <div class=\"modal-header\">\n                        <h5 class=\"modal-title\" id=\"{{ command_action | replace (' ', '') }}ModalLabel\">{{ command_action | capitalize }}</h5>\n                        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n                          <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                      </div>\n                      {% if command_params %}\n                      <div class=\"modal-body text-justify\">\n                        {% for param_name, param_type in command_params.items() %}\n                        <label class=\"required, form-control-label\"\n                               for=\"{{ command_action | replace (' ', '') }}{{ param_name | replace (' ', '') }}input\">\n                            {{ param_name | capitalize }}:\n                        </label>\n                          <input type=\"{{ param_type }}\" class=\"form-control command-param\" data-param-name=\"{{param_name}}\"\n                                 id=\"{{ command_action | replace (' ', '') }}{{ param_name | replace (' ', '') }}input\">\n                        {% endfor %}\n                      </div>\n                      {% endif %}\n                      <div class=\"modal-footer\">\n                        <button class=\"btn btn-primary waves-effect user-command\"\n                            update-url=\"{{ url_for('api.user_command') }}\"\n                            data-action=\"{{ command_action }}\"\n                            data-subject=\"{{ name }}\"\n                            data-dismiss=\"modal\">\n                            {{ command_action | capitalize }}\n                        </button>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n            {% endfor %}\n        </div>\n    </div>\n    {% endif %}\n    <br>\n    {% if not current_profile.read_only and\n        ((tentacle_type == \"trading mode\" and tentacle_desc['requirements']|length > 1) or tentacle_desc['requirements'] == [\"*\"]) %}\n            {{ m_tentacles.missing_tentacles_warning(missing_tentacles) }}\n        <div class=\"card\" id=\"super-container\">\n            <div class=\"card-header\">\n                <h2>Compatible {{\"strategies\" if tentacle_type == \"trading mode\" else \"evaluators\"}}</h2>\n            </div>\n            <div class=\"card-body\" id=\"activatedElementsBody\">\n                {% if tentacle_type == \"trading mode\" %}\n                    {% for evaluator_name, info in strategy_config[\"strategies\"].items() %}\n                        {% if evaluator_name in tentacle_desc['requirements'] %}\n                            {{ m_config_evaluator_card.tentacle_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                        {% endif %}\n                    {% endfor %}\n                {% else %}\n                    {% if \"TA\" in tentacle_desc[\"compatible-types\"] or tentacle_desc[\"compatible-types\"] == [\"*\"]%}\n                    <h2>Technical analysis</h2>\n                        <div>\n                            <div class=\"row config-container\" id=\"ta-evaluator-config-root\">\n                                {% for evaluator_name, info in evaluator_config[\"ta\"].items() %}\n                                    {% if info[\"evaluation_format\"] == \"float\" %}\n                                        {{ m_config_evaluator_card.tentacle_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                                    {% endif %}\n                                {% endfor %}\n                            </div>\n                        </div>\n                        <br>\n                    {% endif %}\n                    {% if \"SOCIAL\" in tentacle_desc[\"compatible-types\"] or tentacle_desc[\"compatible-types\"] == [\"*\"]%}\n                        <h2>Social analysis</h2>\n                        <div>\n                            <div class=\"row config-container\" id=\"social-evaluator-config-root\">\n                                {% for evaluator_name, info in evaluator_config[\"social\"].items() %}\n                                    {% if info[\"evaluation_format\"] == \"float\" %}\n                                        {{ m_config_evaluator_card.tentacle_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                                    {% endif %}\n                                {% endfor %}\n                            </div>\n                        </div>\n                        <br>\n                    {% endif %}\n                    {% if \"REAL_TIME\" in tentacle_desc[\"compatible-types\"] or tentacle_desc[\"compatible-types\"] == [\"*\"]%}\n                        <h2>Real time analysis</h2>\n                        <div>\n                            <div class=\"row config-container\" id=\"rt-evaluator-config-root\">\n                                {% for evaluator_name, info in evaluator_config[\"real-time\"].items() %}\n                                    {% if info[\"evaluation_format\"] == \"float\" %}\n                                        {{ m_config_evaluator_card.tentacle_evaluator_card(evaluator_startup_config, evaluator_name, info, \"evaluator_config\") }}\n                                    {% endif %}\n                                {% endfor %}\n                            </div>\n                        </div>\n                    {% endif %}\n                {% endif %}\n            </div>\n            <div class=\"card-footer\" id='saveConfigFooter'>\n                <button class=\"btn btn-outline-success waves-effect\" id='saveActivationConfig' update-url=\"{{ url_for('advanced.evaluator_config' if tentacle_type == \"strategy\" else 'config') }}\">Save activation and restart later</button>\n            </div>\n        </div>\n        <br>\n    {% endif %}\n    {% if is_trading_strategy_configuration %}\n    <div class=\"card\" id=\"backtestingInputPart\">\n        <div class=\"card-header\" id=\"backtestingPage\" update-url=\"{{ url_for('backtesting', update_type='backtesting_status') }}\">\n            <h2>Test configuration\n                {% if tentacle_desc[\"activation\"] %}\n                    <span class=\"badge badge-primary text-center waves-effect\">Ready to test</span>\n                {% else %}\n                    <a id=\"reloadBacktestingPart\">\n                        <span class=\"badge badge-warning text-center waves-effect\">Activation required <i class=\"fas fa-sync-alt\"></i></span>\n                    </a>\n                {% endif %}\n                <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-advanced-usage/backtesting-and-strategy-optimization?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=config_tentacles\">\n                    &nbsp <i class=\"fa-solid fa-question\"></i>\n                </a>\n                {% if activated_trading_mode %}\n                <a class=\"float-right badge badge-info waves-effect\" href=\"{{ url_for('config_tentacle', name=activated_trading_mode.get_name()) }}\">\n                    <span class=\"d-none d-md-inline\">Current trading mode: </span>{{ activated_trading_mode.get_name() }}\n                </a>\n                {% endif %}\n            </h2>\n        </div>\n        <div class=\"card-body\">\n            <div class=\"row w-100\">\n                {% if activated_trading_mode and activated_trading_mode.is_backtestable() %}\n                    {% if data_files %}\n                        <div class=\"col-12 col-md-6 col-xl-4\">\n                           <select class=\"selectpicker w-100\" data-live-search=\"true\" data-width=\"auto\" data-window-padding=\"25\" id=\"dataFileSelect\"\n                                   {{ \"disabled\" if not tentacle_desc[\"activation\"] }}>\n                            {% for file, description in data_files%}\n                             <option value={{file}}\n                                {% if loop.first %}\n                                    selected=\"selected\"\n                                {% endif %}>\n                               {{\", \".join(description.symbols)}} on {{(description.exchange)}} from the {{(description.date)}}\n                             </option>\n                             {% endfor %}\n                           </select>\n                        </div>\n                        <div class=\"col-12 col-md-6 col-xl-5 row\">\n                            {% if tentacle_desc[\"activation\"] %}\n                                <div class=\"col-12 col-xl-8 d-flex my-auto\">\n                                    <div class=\"mx-1\">\n                                        From :\n                                        <input type=\"date\" class=\"datepicker\" id=\"startDate\">\n                                    </div>\n                                    <div class=\"mx-1\">\n                                        To :\n                                        <input type=\"date\" class=\"datepicker\" id=\"endDate\">\n                                    </div>\n                                </div>\n                                <div class=\"col-12 col-xl-4\">\n                                    <button type=\"button\" id=\"startBacktesting\" class=\"btn btn-primary waves-effect\"\n                                            start-url=\"{{ url_for('backtesting', action_type='start_backtesting', source='config_tentacle', reset_tentacle_config=True) }}\">\n                                        Backtest\n                                    </button>\n                                </div>\n                            {% else %}\n                                <a href=\"{{ url_for('profile') if tentacle_type in [\"trading mode\", \"strategy\"] else url_for('advanced.evaluator_config') }}\"\n                                    role=\"button\" id=\"startBacktesting\" class=\"btn btn-outline-primary waves-effect\">\n                                    Activate this {{ tentacle_type }} to test it\n                                </a>\n                            {% endif %}\n                        </div>\n                    {% else %}\n                        <h4 class=\"py-3 px-3\">\n                            No backtesting data files found. Once you have data files, you will be able to use them here.\n                        </h4>\n                    {% endif %}\n                {% elif activated_trading_mode %}\n                    <div class=\"col-8 alert alert-warning mt-1 text-center\" role=\"alert\">\n                        <a class=\"alert-link\" href=\"{{ url_for('config_tentacle', name=activated_trading_mode.get_name()) }}\">{{ activated_trading_mode.get_name() }}</a> can't be used in backtesting for now.\n                    </div>\n                {% endif %}\n                <div class=\"col-12 col-xl-3\">\n                    <a href=\"{{ url_for('data_collector', from=url_for(request.endpoint, name=name)) }}\" class=\"btn btn-outline-info waves-effect\"> <i class=\"fa fa-cloud-download-alt\"></i> Get historical data</a>\n                </div>\n            </div>\n\n            <span id='backtesting_progress_bar' style='display: none;'>\n                <div class=\"card-title\">\n                    <h2>Backtesting in progress</h2>\n                </div>\n\n                <div class='progress'>\n                  <div id='progess_bar_anim'  class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='0' aria-valuemin='0' aria-valuemax='100' style='width: 0%;'></div>\n                </div>\n            </span>\n        </div>\n    </div>\n    <br>\n    {{ m_backtesting_utils.backtesting_report('config_tentacle', OCTOBOT_DOCS_URL, has_open_source_package) }}\n    {% endif %}\n{% else %}\n    <div class=\"card\">\n        <div class=\"card-header\">\n            <h2>{{ name }}</h2>\n        </div>\n        <div class=\"card-body\">\n          Can't find any tentacle named {{ name }}\n        </div>\n    </div>\n{% endif %}\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/candlesticks.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/tables_display.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/backtesting_util.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/config_tentacle.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/data_collector.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"backtesting\" %}\n{% block body %}\n\n<br>\n\n<div class=\"card\">\n    <div class=\"card-header\"><h2>\n        <span class=\"float-left\">\n            <a href=\"{{ origin_page if origin_page else url_for(\"backtesting\")}}\">\n                <i class=\"fas fa-arrow-left\"></i>\n            </a>\n        </span>\n        &ensp;Download exchange data\n    </h2></div>\n    <div class=\"card-body px-0 px-md-3\">\n        <div class=\"container-fluid row mx-0\">\n            <div class=\"col-12 col-md-10 row\">\n                <div class=\"col-12 col-md-5 row\">\n                    <div class=\"col-6 col-md-3 my-auto\">\n                        Exchange :\n                    </div>\n                    <div class=\"col-6 col-md-9 px-0\">\n                        <select class=\"selectpicker mx-0\"\n                                data-live-search=\"true\"\n                                data-width=\"75%\"\n                                data-window-padding=\"25\"\n                                id=\"exchangeSelect\">\n                            <optgroup label=\"Full History\">\n                                {% for exchange in full_candle_history_ccxt_exchanges %}\n                                <option value={{exchange}}\n                                        {% if exchange==current_exchange %} selected=\"selected\" {% endif %}>\n                                    {{exchange}}\n                                </option>\n                                {% endfor %}\n                            </optgroup>\n                            <optgroup label=\"Other\">\n                                {% for exchange in other_ccxt_exchanges %}\n                                <option value={{exchange}}\n                                        {% if exchange==current_exchange %} selected=\"selected\" {% endif %}>\n                                    {{exchange}}\n                                </option>\n                                {% endfor %}\n                            </optgroup>\n                        </select>\n                    </div>\n                </div>\n                <div class=\"col-12 col-xl-7\">\n                    <div class=\"row pb-1 pb-md-0\" id=\"collector_date_range\">\n                        <div class=\"col-12 col-md-5\">\n                            <div class=\"row\">\n                                <div class=\"col-6 col-md-5 my-auto\">\n                                    Start Date :\n                                </div>\n                                <div class=\"col-6 col-md-7\">\n                                    <input type=\"date\" class=\"datepicker\" id=\"startDate\">\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"col-12 offset-md-1 col-md-5 pt-1 pt-md-0\">\n                            <div class=\"row\">\n                                <div class=\"col-6 col-md-5 my-auto\">\n                                    End Date :\n                                </div>\n                                <div class=\"col-6 col-md-7\">\n                                    <input type=\"date\" class=\"datepicker\" id=\"endDate\">\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"col-12 col-md-5 row\">\n                    <div class=\"col-6 col-md-3 my-auto\">\n                        Pair(s) :\n                    </div>\n                    <div class=\"col-6 col-md-9 px-0\">\n                        <select data-live-search=\"true\"\n                                data-width=\"75%\"\n                                data-window-padding=\"25\"\n                                id=\"symbolsSelect\"\n                                update-url=\"{{ url_for('data_collector', action_type='symbol_list') }}\"\n                                multiple>\n                            {% for symbol in full_symbol_list %}\n                            <option value={{symbol}}>\n                                {{symbol}}\n                            </option>\n                            {% endfor %}\n                        </select>\n                    </div>\n                </div>\n                <div class=\"col-12 col-md-7 pt-1 row pt-md-0\">\n                    <div class=\"col-6 col-md-3 my-auto\">\n                        Time Frame(s) :\n                    </div>\n                    <div class=\"col-6 col-md-5\">\n                        <select data-live-search=\"true\"\n                                data-width=\"80%\"\n                                data-window-padding=\"25\"\n                                id=\"timeframesSelect\"\n                                update-url=\"{{ url_for('data_collector', action_type='available_timeframes_list') }}\"\n                                multiple>\n                            {% for timeframe in available_timeframes_list %}\n                            <option value={{timeframe}}>\n                                {{timeframe}}\n                            </option>\n                            {% endfor %}\n                        </select>\n                    </div>\n                </div>\n            </div>\n            <div class=\"col-12 col-md-2 row\">\n                <div class=\"col-12 text-center\">\n                    <button id=\"collect_data\" type=\"button\" class=\"btn btn-primary waves-effect\"\n                        update-url=\"{{ url_for('data_collector', action_type='start_collector') }}\">\n                        Download\n                    </button>\n                </div>\n                <div class=\"col-12 text-center mt-2\">\n                    <button id=\"stop_collect_data\" type=\"button\" class=\"btn btn-danger waves-effect\"\n                        update-url=\"{{ url_for('data_collector', action_type='stop_collector') }}\">\n                        Cancel\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n    <div id=\"collector_operation\" class=\"p-1\" style='display: none;'>\n        <div id=\"total_progress_bar_anim-container\" class='progress'>\n            <div id='total_progess_bar_anim'\n                 class='progress-bar progress-bar-striped progress-bar-animated'\n                 role='progressbar'\n                 aria-valuenow='0' aria-valuemin='0' aria-valuemax='100'\n                 style='width: 0;'></div>\n        </div>\n        <div id=\"progress_bar_anim-container\" class='progress mt-1'>\n          <div id='current_progess_bar_anim'\n               class='progress-bar progress-bar-striped progress-bar-animated'\n               role='progressbar'\n               aria-valuenow='100' aria-valuemin='0' aria-valuemax='100'\n               style='width: 100%;'></div>\n        </div>\n    </div>\n</div>\n\n<br>\n\n<div class=\"card\">\n    <div class=\"card-header\"><h2>\n        Available backtesting data files\n    </h2></div>\n    <div class=\"card-body pb-0\" id=\"collector_data\">\n        <table class=\"table table-striped table-sm table-responsive-lg\" id=\"dataFilesTable\" update-url=\"{{ url_for('data_collector', action_type='delete_data_file') }}\">\n          <thead>\n            <tr>\n                <th scope=\"col\">Symbol(s)</th>\n                <th scope=\"col\">Date of recording</th>\n                <th scope=\"col\">Candles</th>\n                <th scope=\"col\">Exchange</th>\n                <th scope=\"col\">Time frame(s)</th>\n                <th scope=\"col\">File</th>\n                <th scope=\"col\">Action</th>\n            </tr>\n          </thead>\n          <tbody>\n            {% for file, description in data_files %}\n                <tr class=\"selectable_datafile\">\n                    <td>{{\", \".join(description.symbols)}}</td>\n                    {% if description.start_timestamp %}\n                        <td data-order=\"{{description.timestamp}}\">{{description.start_date}} to {{description.end_date}}</td>\n                        <td>Full</td>\n                    {% else %}\n                        <td data-order=\"{{description.timestamp}}\">{{description.date}}</td>\n                        <td>{{description.candles_length}}</td>\n                    {% endif %}\n                    <td>{{description.exchange}}</td>\n                    <td>{{\", \".join(description.time_frames)}}</td>\n                    <td>{{file}}</td>\n                    <td class=\"text-center\">\n                        <a class=\"btn btn-outline-primary delete_data_file waves-effect\" data-file={{file}} data-toggle=\"tooltip\" data-placement=\"right\" title=\"Delete data file\">\n                            <i class=\"fas fa-trash\"></i>\n                        </a>\n                    </td>\n                </tr>\n            {% endfor %}\n          </tbody>\n        </table>\n    </div>\n    {% if not has_open_source_package() %}\n    <div class=\"card-footer text-center\">\n        <i class=\"fa fa-info-circle\"></i> <a href=\"{{ url_for('extensions') }}\">Strategy designer</a> data files are reused and kept up-to-date to lower download time.\n    </div>\n    {% endif %}\n</div>\n\n<br>\n\n<div class=\"card\">\n    <div class=\"card-header\"><h2>Import from data file</h2></div>\n    <div class=\"card-body\">\n        <form id=\"importForm\" action = \"{{ url_for('data_collector', action_type='import_data_file') }}\" method = \"POST\"\n         enctype = \"multipart/form-data\">\n            <div class=\"custom-file\">\n                <input type=\"file\" class=\"custom-file-input\" id=\"inputFile\" name=\"file\" accept=\".data\">\n                <label class=\"custom-file-label\" for=\"inputFile\" id=\"inputFileLabel\">Choose data file</label>\n            </div>\n            <button class=\"btn btn-primary waves-effect mt-2\" type=\"submit\" id=\"importFileButton\">Import data file</button>\n        </form>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/data_collector_util.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/data_collector.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% if alert %}\n<script>\n    display_alert(\"{{alert[\"success\"]}}\", \"{{alert[\"message\"]}}\");\n</script>\n{% endif %}\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/default/footer.html",
    "content": "<!-- Footer -->\n<footer class=\"page-footer font-small fixed-bottom\">\n\n    <div class=\"footer-copyright text-center py-sm-2 py-0\">Follow the\n        <a href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=footer\" target=\"_blank\" rel=\"noopener\"> OctoBot</a> <span class=\"d-none d-md-inline\">updates </span>on\n        <a href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/blog?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=footer\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fas fa-newspaper\"></i><span class=\"d-none d-md-inline\"> our Blog</span></a>\n        <a href=\"https://github.com/Drakkar-Software/OctoBot\" target=\"_blank\" rel=\"noopener\"><i class=\"fab fa-github\"></i><span class=\"d-none d-md-inline\"> GitHub</span></a>\n        <a href=\"https://twitter.com/DrakkarsOctoBot\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-twitter\"></i><span class=\"d-none d-md-inline\"> Twitter</span></a>\n        <a href=\"https://t.me/OctoBot_Project\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-telegram\"></i><span class=\"d-none d-md-inline\"> Telegram</span></a>\n        <a href=\"https://discordapp.com/invite/vHkcb8W\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-discord\"></i><span class=\"d-none d-md-inline\"> Discord</span></a>\n        <a href=\"https://www.youtube.com/channel/UC2YAaBeWY8y_Olqs79b_X8A\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-youtube\"></i><span class=\"d-none d-md-inline\"> YouTube</span></a>\n        Join the <a href=\"https://t.me/joinchat/F9cyfxV97ZOaXQ47H5dRWw\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-telegram\"></i>\n        <span class=\"d-none d-md-inline\">OctoBot</span> community <span class=\"d-none d-md-inline\">chat</span></a> <span class=\"d-none d-md-inline\">for the best tips and tricks.</span>\n        &emsp;\n        <span>OctoBot {{ CURRENT_BOT_VERSION }}</span>\n  </div>\n\n</footer>\n<!-- Footer -->"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/default/navbar.html",
    "content": "<!-- Navbar -->\n<nav class=\"navbar navbar-expand-lg sticky-top py-0 py-md-2\" id=\"main-nav-bar\">\n    <div class=\"navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2\">\n        <ul class=\"navbar-nav mr-auto\" id=\"main-nav-left-part\">\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'home' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('home') }}\">Home</a>\n            </li>\n            <li id=\"main-nav-trading\" class=\"nav-item mx-1 px-0 my-auto {% if 'trading' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('trading') }}\">Trading</a>\n            </li>\n            <li id=\"main-nav-portfolio\" class=\"nav-item mx-1 px-0 my-auto {% if 'portfolio' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('portfolio') }}\">Portfolio</a>\n            </li>\n            <li id=\"main-nav-profile\" class=\"nav-item mx-1 px-0 my-auto {% if 'profile' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('profile') }}\">Profile</a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'accounts' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('accounts') }}\">Accounts</a>\n            </li>\n            {% if not has_open_source_package() %}\n                <li class=\"nav-item mx-1 px-0 my-auto\">\n                    <a class=\"nav-link\" href=\"{{ url_for('extensions') }}\"><i class=\"fa-solid fa-lock\"></i> Strategy design</a>\n                </li>\n            {% endif %}\n            {% for plugin_tab in get_plugin_tabs(TAB_START) %}\n            <li class=\"nav-item mx-1 px-0 my-auto {% if plugin_tab.identifier == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for(plugin_tab.route) }}\">{{ plugin_tab.display_name }}</a>\n            </li>\n            {% endfor %}\n        </ul>\n    </div>\n    <div class=\"d-flex mx-auto order-0\">\n        <a class=\"navbar-brand mx-auto font-weight-bolder\" href=\"{{ url_for('home') }}\">\n            {% if is_in_stating_community_env() %}\n            <span class=\"badge badge-light\">\n                Beta\n            </span>\n            {% endif %}\n            <img\n                src=\"{{url_for('static', filename='img/octobot-logo-'+get_color_mode()+'.png')}}\" alt=\"octobot-logo\"\n                class=\"navbar-logo\"\n            > OctoBot\n            <i id=\"navbar-bot-status\" class=\"ml-2 fa fa-check\" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"OctoBot operational\"></i>\n        </a>\n        <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".dual-collapse2\">\n            <span class=\"navbar-toggler-icon\"></span>\n        </button>\n    </div>\n    <div class=\"navbar-collapse collapse w-100 order-3 dual-collapse2\">\n        <ul class=\"navbar-nav ml-auto\" id=\"main-nav-right-part\">\n            {% if (not is_login_required()) or (is_login_required() and is_authenticated()) %}\n            <li class=\"nav-item mx-1 px-0 my-auto\" id=\"main-nav-trading-type\">\n                <a class=\"nav-link\" href=\"#\" id=\"switchTradingState\" aria-label=\"TradingSwitch\">\n                    {{ m_trading_state.display_trading_state(is_real_trading(get_current_profile()), get_enabled_trader(get_current_profile())) }}\n                </a>\n            </li>\n            {% endif %}\n            {% for plugin_tab in get_plugin_tabs(TAB_END) %}\n            <li class=\"nav-item mx-1 px-0 my-auto {% if plugin_tab.identifier == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for(plugin_tab.route) }}\">{{ plugin_tab.display_name }}</a>\n            </li>\n            {% endfor %}\n            {% if is_backtesting_enabled %}\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'backtesting' == active_page %} active{% endif %}\"\n                id=\"main-nav-backtesting\">\n                <a class=\"nav-link\" href=\"{{ url_for('backtesting') }}\">Backtesting</a>\n            </li>\n            {% endif %}\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'community' == active_page %} active{% endif %}\"\n                id=\"main-nav-community\">\n                <a class=\"nav-link\" href=\"{{ url_for('community') }}\">Community</a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'help' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('octobot_help') }}\">Help</a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'about' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('about') }}\">About</a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'logs' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('logs') }}\" aria-label=\"Logs\"><i class=\"fa fa-bell\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Event logs\">\n                    <span id=\"errors-count-badge\" class=\"badge badge-warning\"></span>\n                </i></a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'logs' == active_page %} active{% endif %}\">\n                <a id=\"theme-switch\" class=\"nav-link\" href=\"#\" aria-label=\"Switch theme\" data-update-url=\"{{url_for('api.display_config')}}\">\n                    <i class=\"{{'fa fa-moon' if get_color_mode() == 'light' else 'fas fa-sun'}}\" data-toggle=\"tooltip\" data-placement=\"top\"\n                       title=\"Use {{'dark' if get_color_mode() == 'light' else 'light'}} theme\">\n                </i></a>\n            </li>\n\n\n            {% if is_advanced_interface_enabled %}\n            <li class=\"nav-item my-auto\">\n                <a class=\"nav-link mx-1 px-0\" href=\"{{ url_for('advanced.home') }}\" aria-label=\"Advanced OctoBot\"><i class=\"fa fa-cogs\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Advanced OctoBot\"></i></a>\n            </li>\n            {% endif %}\n        </ul>\n    </div>\n</nav>\n<!-- Navbar -->"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/cloud.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"cloud\" %}\n\n{% block body %}\n<br>\n\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>\n            The next level of market making, with the transparency of OctoBot\n        </h2>\n    </div>\n    <div class=\"card-body\">\n\n        <div class=\"py-4 row\">\n            <div class=\"col\">\n                <p>\n                    <a target=\"_blank\" rel=\"noopener\"\n                           href=\"{{OCTOBOT_MARKET_MAKING_URL}}?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_header_text\">\n                        OctoBot cloud Market Making</a>\n                    is a self-service market making automation platform. It is based on an extension of this open source\n                    market making distribution of the <a target=\"_blank\" rel=\"noopener\"\n                           href=\"{{OCTOBOT_COMMUNITY_URL}}/trading-bot?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_header_text\">\n                        OctoBot trading robot</a>,\n                    which is being actively developed since 2018.\n                </p>\n                <p>\n                    With OctoBot cloud Market Making:\n                    <ul>\n                        <li>\n                            Benefit from advanced liquidity monitoring and strategies\n                        </li>\n                        <li>\n                            Let yourself be guided to configure strategies tailored for your market and exchange\n                        </li>\n                        <li>\n                            Adapt your strategies according to your needs at all time\n                        </li>\n                    </ul>\n                </p>\n                <p>\n                    Using OctoBot cloud for market making is an alternative to this free market making\n                    distribution of OctoBot. While it provides more capabilities and advanced features, the\n                    software you are currently using is and will remain free to help smaller projects and individuals\n                    benefit from market making automation in a simple way.\n                </p>\n            </div>\n        </div>\n        <div class=\"row mt-5\">\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Follow your market's liquidity\n                </h3>\n                <p>\n                    Exchange markets liquidity is measured using via the <a target=\"_blank\" rel=\"noopener\"\n                           href=\"{{OCTOBOT_MARKET_MAKING_URL}}/{{LOCALE}}/guides/understanding-the-liquidity-score?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_text\">\n                        OctoBot Liquidity Score</a>.\n                    This is a 0 to 10 value measuring how well adapted to the trading demand an order book is.\n                </p>\n                <p>\n                    The Liquidity Score:\n                    <ul>\n                        <li>\n                            Makes it easy to measure your token's liquidity on each exchange\n                        </li>\n                        <li>\n                            Is automatically adapted to the volume requirement of each market\n                        </li>\n                        <li>\n                            Gives you clear insights on the markets you follow\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_MARKET_MAKING_URL}}/{{LOCALE}}/guides/understanding-the-liquidity-score?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_button\">\n                        Learn more on the Liquidity Score\n                    </a>\n                </div>\n            </div>\n            <div class=\"col-12 col-lg-6 text-center\">\n                 <img class=\"img-fluid img-feature\"\n                      src=\"{{url_for('static', filename='img/distributions/market_making/octobot-market-making-liquidity-scores-preview.jpg')}}\"\n                      alt=\"octobot market making liquidity scores preview\">\n           </div>\n        </div>\n        <div class=\"row feature-margin\">\n            <div class=\"col-12 col-lg-6\">\n                <img class=\"img-fluid img-feature\"\n                      src=\"{{url_for('static', filename='img/distributions/market_making/configuration-preview.png')}}\"\n                      alt=\"configuration preview\">\n            </div>\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Start your advanced market making strategy\n                </h3>\n                <p>\n                     <a target=\"_blank\" rel=\"noopener\"\n                           href=\"{{OCTOBOT_MARKET_MAKING_URL}}?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_text\">\n                        OctoBot cloud Market Making</a>\n                    provides you with more advanced market making strategies and makes it easier to configure and visualize\n                    your strategy using the built-in order book preview.\n                <p>\n                </p>\n                <p>\n                    More capabilities for better market making:\n                    <ul>\n                        <li>\n                            Unlimited orders in the book and order book profiles\n                        </li>\n                        <li>\n                            Ready-made configurations adapted to your market volume\n                        </li>\n                        <li>\n                            Custom budgeting & automated adjustment upon market volume ups and downs to optimize your funds\n                        </li>\n                    </ul>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_MARKET_MAKING_URL}}/{{LOCALE}}/guides/starting-your-market-making-bot?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=cloud_button\">\n                        Learn more on advanced strategies\n                    </a>\n                </div>\n            </div>\n        </div>\n        <div class=\"row feature-margin\">\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Your self-service market making platform\n                </h3>\n                <p>\n                    Create the right market making strategy for your market by yourself. The OctoBot Market Making team\n                    is here to answer your questions and take care of the technical aspect of the bot.\n                </p>\n                <p>\n                    With improved crypto baskets :\n                    <ul>\n                        <li>\n                            Personalized support sessions with the team to make the most of the platform\n                        </li>\n                        <li>\n                            Follow your bot and adapt your strategy within seconds\n                        </li>\n                        <li>\n                            Your bot operational at all time on our secure cloud\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_MARKET_MAKING_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n                        Learn more on OctoBot Market Making\n                    </a>\n                </div>\n            </div>\n            <div class=\"col-12 col-lg-6\">\n                <img class=\"img-fluid img-feature\"\n                      src=\"{{url_for('static', filename='img/distributions/market_making/live-bot-preview.png')}}\"\n                      alt=\"live bot preview\">\n            </div>\n        </div>\n        <div class=\"feature-margin d-flex justify-content-center\">\n            <div class=\"mx-auto\">\n                <h3 class=\"text-center\">\n                    And more\n                </h3>\n\n                <p class=\"\">\n                    <ul>\n                        <li>\n                            Direct communication channel to quickly answer your questions\n                        </li>\n                        <li>\n                            Priority new features and improvements requests\n                        </li>\n                        <li>\n                            <a  target=\"_blank\" rel=\"noopener\"\n                                href=\"https://www.hollaex.com/blog/introducing-market-making-bots-for-crypto-pro-above-simplifying-new-market-creation\">\n                                HollaEx-powered exchanges\n                            </a> built-in support\n                        </li>\n                    </ul>\n                </p>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"pb-5 text-center\">\n        <a type=\"button\" class=\"btn btn-primary\" target=\"_blank\" rel=\"noopener\"\n           href=\"{{OCTOBOT_MARKET_MAKING_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n            Go to OctoBot Market Making\n        </a>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/extensions.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/cloud_features.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"community\" %}\n{% import \"components/community/tentacle_packages.html\" as m_tentacle_packages %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n\n{% block body %}\n<br>\n\n{{ m_flash_messages.flash_messages() }}\n{{ m_tentacle_packages.pending_tentacles_install_modal(has_owned_packages_to_install) }}\n{{ m_tentacle_packages.waiting_for_owned_packages_to_install_modal(not has_owned_packages_to_install and auto_refresh_packages) }}\n{{ m_tentacle_packages.select_payment_method_modal(OCTOBOT_EXTENSION_PACKAGE_1_NAME) }}\n\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>\n            Improve your bot with the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}\n        </h2>\n    </div>\n    <div class=\"card-body\">\n\n        <div class=\"py-5 row\">\n            <div class=\"col\">\n                <p>\n                    The {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} improves your OctoBot\n                    capabilities and simplifies its use.\n                </p>\n                <p>\n                    It adds the <strong>Strategy Designer</strong> to your OctoBot,\n                    greatly <strong>simplifies the TradingView</strong> configuration, improves <strong>crypto basket</strong>\n                    investments much more.\n                </p>\n                <p>\n                    The extension is completely optional and bound to your OctoBot account. This means that updating or\n                    reinstalling your OctoBot will automatically install your extension as long as you are\n                    connected to your OctoBot account.\n                </p>\n            </div>\n            <div class=\"col text-center my-auto\">\n                <div class=\"text-center\">\n                    <h5>${{price}}</h5>\n                </div>\n                <div>\n                    Lifetime improvement of your OctoBot.\n                </div>\n                <div>\n                    No subscription. Pay with crypto or credit card.\n                </div>\n                <div class=\"mt-4\">\n                    {{ m_tentacle_packages.get_package_button(OCTOBOT_EXTENSION_PACKAGE_1_NAME, is_community_authenticated, has_open_source_package) }}\n                </div>\n                {% if is_community_authenticated %}\n                <div class=\"\">\n                    Authenticated as {{current_logged_in_email}}\n                </div>\n                {% endif %}\n            </div>\n        </div>\n        <div class=\"row mt-5\">\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Optimize your own strategies with the Strategy Designer\n                </h3>\n                <p>\n                    The <a target=\"_blank\" rel=\"noopener\"\n                           href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-usage/strategy-designer?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_text\">\n                        Strategy Designer\n                    </a>\n                    is OctoBot's most advanced strategy optimization and backtesting interface.\n                </p>\n                <p>\n                    Using the Strategy Designer, you can:\n                    <ul>\n                        <li>\n                            Compare backtesting results of your strategies\n                        </li>\n                        <li>\n                            Visualize your strategies behavior through time\n                        </li>\n                        <li>\n                            Optimize your strategy while using a different live profile\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-usage/strategy-designer?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n                        Learn more on the Strategy Designer\n                    </a>\n                </div>\n            </div>\n            <div class=\"col-12 col-lg-6 text-center\">\n                <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/V4Z1xUhqWz8?showinfo=0&amp;rel=0\"\n                title=\"The OctoBot Strategy Designer\" frameborder=\"0\" allow=\"accelerometer; autoplay;\n                clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n            </div>\n        </div>\n        <div class=\"row feature-margin\">\n            <div class=\"col-12 col-lg-6\">\n                <img class=\"img-fluid img-feature\"\n                         src=\"{{url_for('static', filename='img/community/tentacles_packages_previews/tradingview-ema-strategy-illustration-with-2-buy-and-2-sell.png')}}\" alt=\"OctoBot cloud\">\n            </div>\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Seamlessly connect your TradingView webhook\n                </h3>\n                <p>\n                    In the default version of OctoBot, advanced technical knowledge or a paid external\n                    webhook provider such as Ngrok is required to connect to your TradingView alerts.\n                <p>\n                </p>\n                    Using OctoBot with the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} makes TradingView strategies automation:\n                    <ul>\n                        <li>\n                            Free: No TradingView subscription is required to use email alerts, no Ngrok subscription is needed for webhooks\n                        </li>\n                        <li>\n                            Easy: You get a unique alert email address and webhook URL you can use rightaway\n                        </li>\n                        <li>\n                            Secure: Use the OctoBot cloud secure email and webhook system\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-trading-modes/tradingview-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_text\">\n                        Learn more on TradingView Strategies\n                    </a>\n                </div>\n            </div>\n        </div>\n        <div class=\"row feature-margin\">\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Use and configure your improved crypto baskets\n                </h3>\n                <p>\n                    OctoBot cloud <a target=\"_blank\" rel=\"noopener\"\n                                     href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/features/crypto-basket?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_text\">\n                        crypto baskets\n                    </a>\n                    are special strategies that make it easy to invest in the top crypto of the market or specific themes.\n                </p>\n                <p>\n                    With improved crypto baskets :\n                    <ul>\n                        <li>\n                            Follow automatically updated OctoBot cloud crypto baskets\n                        </li>\n                        <li>\n                            Or start from an existing basket and customize it\n                        </li>\n                        <li>\n                            Watch your portfolio automatically follow the latest trends\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_COMMUNITY_URL}}/features/crypto-basket?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n                        Learn more about crypto baskets\n                    </a>\n                </div>\n            </div>\n            <div class=\"col-12 col-lg-6\">\n                <img class=\"img-fluid img-feature\"\n                             src=\"{{url_for('static', filename='img/community/tentacles_packages_previews/crypto-basket.png')}}\" alt=\"OctoBot cloud\">\n            </div>\n        </div>\n        <div class=\"feature-margin d-flex justify-content-center\">\n            <div class=\"mx-auto\">\n                <h3 class=\"text-center\">\n                    And more!\n                </h3>\n                <p class=\"text-center\">\n                    With the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} :\n                </p>\n\n                <p class=\"\">\n                    <ul>\n                        <li>\n                            Invest using exclusive strategies and profiles\n                        </li>\n                        <li>\n                            Join the exclusive Discord channel\n                        </li>\n                    </ul>\n                </p>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"py-4 text-center\">\n        <div>\n            {{ m_tentacle_packages.get_package_button(OCTOBOT_EXTENSION_PACKAGE_1_NAME, is_community_authenticated, has_open_source_package) }}\n        </div>\n        <div class=\"mt-5\">\n            Any question on the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} ? Just\n            <a href=\"mailto:contact@octobot.cloud?subject=About the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}\">ask the team</a>.\n        </div>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/extensions.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/configuration.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"configuration\" %}\n\n{% import 'components/config/tentacle_config_editor.html' as m_tentacle_config_editor %}\n\n\n{% macro save_block(display_interfaces_link) %}\n<div class=\"d-flex justify-content-between\">\n    {% if display_interfaces_link %}\n    <div>\n        <a href=\"{{url_for('interfaces')}}\">\n            <button type=\"button\" class=\"btn btn-outline-primary btn-lg\"\n                    data-toggle=\"tooltip\" title=\"Configure your OctoBot's web interface access and enable the Telegram bot.\">\n                Web & Telegram configuration\n            </button>\n        </a>\n    </div>\n    {% endif %}\n    <div class=\"\">\n        <button type=\"button\" class=\"btn btn-primary btn-lg\"\n                data-role=\"save\" data-update-url=\"{{url_for('save_market_making_config')}}\">\n            <i class=\"fas fa-save\" aria-hidden=\"true\"></i> Save\n        </button>\n    </div>\n    <div>\n        <button type=\"button\" class=\"btn btn-outline-primary btn-lg\" route=\"{{ url_for('commands', cmd='restart') }}\">\n            <i class=\"fa fa-refresh\" aria-hidden=\"true\"></i> Restart\n        </button>\n    </div>\n</div>\n{% endmacro %}\n\n{% block body %}\n<br/>\n<div class=\"card card-body\">\n    <div class=\"grid row\">\n        <div class=\"col col-lg-6 markdown-content\">\n            {{tentacle_docs}}\n        </div>\n        <div class=\"col col-lg-6\">\n            <div id=\"exchange-and-pair\">\n                <h2>Exchange and Trading pair</h2>\n                <div class=\"grid row form-inline my-4\">\n                    <div class=\"col col-lg-6\">\n                        <div class=\"input-group\">\n                            <label for=\"main-exchange-selector\" class=\"font-weight-bolder\">Exchange</label>\n                            <select id=\"main-exchange-selector\" class=\"form-control ml-1 mx-lg-4\"\n                                    data-selected-exchange=\"{{selected_exchange}}\"></select>\n                        </div>\n                    </div>\n                    <div class=\"col col-lg-6\">\n                        <div class=\"input-group\">\n                            <label for=\"traded-symbol-selector\" class=\"font-weight-bolder\">Trading pair</label>\n                            <select id=\"traded-symbol-selector\" class=\"form-control ml-1 mx-lg-4\" data-selected-pair=\"{{selected_pair}}\"\n                                    data-update-url=\"{{ url_for('api.get_all_symbols', exchange='') }}\"></select>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div id=\"trading-mode-config-editor\" data-trading-mode-name=\"{{trading_mode_name}}\">\n                {{ m_tentacle_config_editor.tentacles_config_editor(trading_mode_name) }}\n            </div>\n            <div class=\"mx-3\">\n                {{ save_block(False) }}\n            </div>\n        </div>\n    </div>\n    <div id=\"trading-simulation\">\n        <h2>Trading simulator configuration</h2>\n        <div class=\"grid row\">\n            <div class=\"col col-lg-6\">\n                <div id=\"trading-simulator-editor\"\n                     data-config='{{config_trading_simulator | tojson}}'\n                     data-schema='{{trading_simulator_schema | tojson}}'\n                ></div>\n            </div>\n            <div class=\"col col-lg-6 pt-lg-5\">\n                <div id=\"simulated-portfolio-editor\"\n                     data-config='{{simulated_portfolio | tojson}}'\n                     data-schema='{{portfolio_schema | tojson}}'\n                ></div>\n            </div>\n        </div>\n    </div>\n    <div id=\"exchange-configuration\">\n        <h2>Exchanges configuration</h2>\n        <div class=\"mt-4\">\n            <p>\n                Add the exchange to perform market making on as well as the exchange used as \"Reference exchange\".\n            </p>\n            <div>\n                Note: For trading simulator and <strong>reference exchanges</strong>, exchanges must be added, and\n                <strong>API details are not required</strong>.\n            </div>\n        </div>\n        <div id=\"exchanges-editor\"\n             data-config='{{config_exchanges | tojson}}'\n             data-schema='{{exchanges_schema | tojson}}'\n        ></div>\n        <p class=\"mx-4\">\n            <i>Click save after adding a new exchange to be able to select it.</i>\n        </p>\n    </div>\n    {{ save_block(True) }}\n\n    <span class=\"d-none\"\n          data-display-intro=\"{{display_intro}}\"\n    ></span>\n</div>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='distributions/market_making/js/configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/config_tentacle.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/dashboard.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"home\" %}\n\n{% import 'macros/critical_notifications_alert.html' as m_critical_notifications_alert %}\n\n{% macro waiter(waiter_id, title) %}\n<div id=\"{{waiter_id}}\" class=\"text-center my-4 py-4 h-100\">\n    <div class=\"py-4\">\n        <h2>{{title}}</h2>\n    </div>\n    <div class=\"py-4\">\n        <h2><i class=\"fa fa-spinner fa-spin\"></i></h2>\n    </div>\n</div>\n{% endmacro %}\n\n\n{% block body %}\n    <br>\n\n    {% if display_ph_launch %}\n    <div class=\"alert alert-primary text-center\" role=\"alert\">\n        <h5 class=\"\">\n            🎉 Major news! OctoBot is launching on Product Hunt {{'today' if is_launching else 'on the 10th of July'}}.\n            {% if not is_launching %}\n            <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\" data-role=\"hide-announcement\" data-url=\"{{url_for('api.hide_announcement', key='product_hunt_announcement')}}\">\n                <span aria-hidden=\"true\">&times;</span>\n            </button>\n            {% endif %}\n        </h5>\n        <p class=\"text-center pt-3 mb-0\">\n            <a href=\"https://www.producthunt.com/posts/octobot-open-source?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-octobot&#0045;open&#0045;source\" target=\"_blank\"><img src=\"https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=463219&theme={{get_color_mode()}}\" alt=\"OctoBot&#0032;open&#0032;source - Your&#0032;open&#0032;source&#0032;investment&#0032;strategy&#0032;builder | Product Hunt\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" /></a>\n\n        </p>\n        <p class=\"text-center pt-3 mb-0\">\n            Follow the launch to get your exclusive discount.\n        </p>\n    </div>\n    {% endif %}\n    {{ m_critical_notifications_alert.critical_notifications_alert(critical_notifications) }}\n    <span id=\"exchange-specific-data\">\n        <div class=\"card\" id=\"dashboard-graph\">\n            <div class=\"card-header d-flex justify-content-between\" id=\"all-watched-markets\">\n                <div>\n                    {% if sandbox_exchanges %}\n                    <h5>\n                        <span class=\"badge badge-warning\"\n                              id=\"sandbox-badge\"\n                              data-toggle=\"tooltip\"\n                              title=\"{{ sandbox_exchanges | join(' and ')}} testnet / sandbox is enabled. This means that\n                              your OctoBot is trading using testnet prices and accounts, which\n                              might not be representative of real markets. Use the trading simulator to test a trading\n                              strategy in real conditions.\"\n                        >{{sandbox_exchanges[0] | capitalize}} sandbox</span>\n                    </h5>\n                    {% endif %}\n                </div>\n                <h4>Market making enabled</h4>\n                <div>\n                    <a class=\"waves-effect float-right\" href=\"#\" id=\"display-dashboard-settings-modal-label\"\n                       data-toggle=\"modal\" data-target=\"#dashboard-settings-modal\">\n                       <i class=\"fa fa-1_5x fa-cog\"></i>\n                    </a>\n                </div>\n            </div>\n            <div class=\"card-body d-none text-center\" id=\"loadingMarketsDiv\">\n                <h2>\n                    OctoBot is starting, markets will be refreshed when exchanges will be reachable.\n                </h2>\n            </div>\n            <div class=\"card-body text-center\" name=\"loadingSpinner\">\n                <h2>\n                    <i class=\"fa fa-spinner fa-spin\"></i>\n                </h2>\n                <p class=\"mt-5\">\n                    If this loader remains, please make sure that at least one exchange is enabled in\n                    <a href=\"{{url_for('configuration', _anchor='panelExchanges')}}\">your profile</a>.\n                </p>\n            </div>\n            <div class=\"card-body candle-graph d-none\" id=\"first_symbol_graph\" update-url=\"{{ url_for('first_symbol') }}\">\n                <div id=\"graph-symbol-price\"></div>\n            </div>\n        </div>\n        <div class=\"modal\" id=\"dashboard-settings-modal\" tabindex=\"-1\" role=\"dialog\"\n             aria-labelledby=\"#display-dashboard-settings-modal-label\" aria-hidden=\"true\">\n          <div class=\"modal-dialog modal-dialog-centered modal-md\" role=\"document\">\n            <div class=\"modal-content modal-text\">\n              <div class=\"modal-header primary-text\">\n                <h2 class=\"modal-title\">Settings</h2>\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n                      <span aria-hidden=\"true\">&times;</span>\n                    </button>\n              </div>\n              <div class=\"modal-body\">\n                  <div class=\"mx-4\">\n                    <label for=\"timeFrameSelect\">Dashboard time frame</label>\n                    <select class=\"selectpicker\" id=\"timeFrameSelect\" data-live-search=\"true\" data-update-url=\"{{url_for('api.display_config')}}\">\n                      {% for time_frame in all_time_frames %}\n                        <option value=\"{{time_frame.value}}\" {{'selected=\"selected\"' if time_frame.value == display_time_frame}}>\n                            {{time_frame.value}}\n                        </option>\n                      {% endfor %}\n                    </select>\n                    <div class=\"custom-control custom-switch my-auto\" id=\"synchronized-data-only-div\">\n                        <input type=\"checkbox\" class=\"custom-control-input\" id=\"displayOrderToggle\" {{'checked' if display_orders}}  data-update-url=\"{{url_for('api.display_config')}}\">\n                        <label class=\"custom-control-label\" for=\"displayOrderToggle\">Display orders</label>\n                    </div>\n                  </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <br>\n\n        <div class=\"card\">\n            <div class=\"card-header text-center\">\n                <h4>Open orders</h4>\n            </div>\n            <div class=\"card-body\">\n                {{ waiter(\"orders-waiter\", \"Loading orders\") }}\n                <div class='progress mb-1' id='cancel_order_progress_bar' style='display: none;'>\n                    <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='100' aria-valuemin='0' aria-valuemax='100' style='width: 100%;'></div>\n                </div>\n                <div id=\"openOrderTable\">\n                  <table id=\"orders-table\"\n                         data-url=\"{{url_for('api.orders')}}\"\n                         class=\"w-100 table-striped table-responsive-sm\">\n                  </table>\n                </div>\n            </div>\n        </div>\n\n        <br>\n\n        <div class=\"card\" id=\"profitability-display\">\n            <div class=\"card-header\">\n                <h4>\n                    <div class=\"row\">\n                        <div class=\"d-none d-md-flex col-md-3\">\n                            Portfolio value\n                        </div>\n                        <div class=\"col-10 col-md-6 text-md-center\">\n                            <span class=\"d-none align-middle\"\n                                  id=\"flat-profitability\">\n                                <span id=\"flat-profitability-text\">\n                                </span>\n                                {{reference_unit}}\n                            </span>\n                            <span class=\"badge d-none\"\n                                  id=\"profitability-badge\"\n                                  data-toggle=\"tooltip\"\n                                  title=\"Portfolio total value change since the first valuation. You can reset it from the portfolio tab.\"\n                            ><span id=\"profitability-value\"></span>%</span>\n                        </div>\n                        <div class=\"col-2 offset-md-1 text-right px-0\">\n                            <a class=\"blue-text\"\n                               href=\"{{url_for('portfolio')}}\"\n                               data-toggle=\"tooltip\"\n                               title=\"Portfolio details\">\n                                <i class=\"fas fa-chart-pie\"></i>\n                            </a>\n                        </div>\n                    </div>\n                </h4>\n            </div>\n            <div class=\"card-body card-text\" id=\"profitability_graph\">\n                <div class=\"w-100\">\n                    <div id=\"portfolio_historyChart\"\n                         data-url=\"{{url_for('api.historical_portfolio_value', currency=reference_unit, time_frame='')}}\"\n                         data-reference-market=\"{{reference_unit}}\"\n                         class=\"w-100\">\n                    </div>\n                </div>\n            </div>\n            <div class=\"card-body card-text d-none\" id=\"no_profitability_graph\">\n                Your daily portfolio value history will be displayed here.\n            </div>\n        </div>\n    </span>\n\n    <br>\n\n    <div class=\"card\">\n        <div class=\"card-header text-center\">\n            <h4>Trades history</h4>\n        </div>\n        <div class=\"card-body\">\n            {{ waiter(\"trades-waiter\", \"Loading trades\") }}\n            <table id=\"trades-table\"\n                 data-url=\"{{url_for('api.trades')}}\"\n                 data-reference-market=\"{{reference_market}}\"\n                 class=\"w-100 table-striped table-responsive-sm\">\n            </table>\n        </div>\n    </div>\n\n    <div class=\"text-right my-5\">\n        <button type=\"button\" class=\"btn btn-outline-primary btn-lg\" route=\"{{ url_for('commands', cmd='stop') }}\">\n            <i class=\"fa fa-power-off\" aria-hidden=\"true\"></i> Stop OctoBot\n        </button>\n    </div>\n\n    <span class=\"d-none\"\n          data-display-intro=\"{{display_intro}}\"\n    ></span>\n{% endblock %}\n\n{% block additional_scripts %}\n    <script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/common/candlesticks.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/common/portfolio_history.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/components/dashboard.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/components/tentacles_configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/common/tables_display.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/components/trading.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='distributions/market_making/js/dashboard.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/footer.html",
    "content": "<!-- Footer -->\n<footer class=\"page-footer font-small fixed-bottom\">\n\n    <div class=\"footer-copyright text-center py-sm-2 py-0\">Follow the\n        <a href=\"{{OCTOBOT_MARKET_MAKING_URL}}?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=footer\" target=\"_blank\" rel=\"noopener\"> OctoBot Market Making</a> <span class=\"d-none d-md-inline\">updates </span>on\n        <a href=\"{{OCTOBOT_MARKET_MAKING_URL}}/{{LOCALE}}/blog?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=footer\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fas fa-newspaper\"></i><span class=\"d-none d-md-inline\"> our Blog</span></a>\n        <a href=\"https://github.com/Drakkar-Software/OctoBot\" target=\"_blank\" rel=\"noopener\"><i class=\"fab fa-github\"></i><span class=\"d-none d-md-inline\"> GitHub</span></a>\n        <a href=\"https://twitter.com/DrakkarsOctoBot\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-twitter\"></i><span class=\"d-none d-md-inline\"> Twitter</span></a>\n        <a href=\"https://t.me/OctoBot_Project\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-telegram\"></i><span class=\"d-none d-md-inline\"> Telegram</span></a>\n        <a href=\"https://discordapp.com/invite/vHkcb8W\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-discord\"></i><span class=\"d-none d-md-inline\"> Discord</span></a>\n        Join the <a href=\"https://t.me/joinchat/F9cyfxV97ZOaXQ47H5dRWw\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fab fa-telegram\"></i>\n        <span class=\"d-none d-md-inline\">OctoBot</span> community <span class=\"d-none d-md-inline\">chat</span></a> <span class=\"d-none d-md-inline\">for the best tips and tricks.</span>\n        &emsp;\n        <span>OctoBot {{ CURRENT_BOT_VERSION }}</span>\n  </div>\n\n</footer>\n<!-- Footer -->"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/interfaces.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"configuration\" %}\n{% import 'components/config/service_card.html' as m_config_service_card %}\n{% import 'components/config/notification_config.html' as m_config_notification %}\n\n{% block additional_style %}\n    <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/components/configuration.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n{% endblock additional_style %}\n\n{% block body %}\n<div class=\"row mt-md-4 mt-2\">\n  <nav class=\"mt-md-4 mt-2 col-md-3 col-lg-2 col-1 d-block sidebar shadow\">\n      <div class=\"sidebar-sticky mt-0 pt-0\">\n        <div class=\"col-8 d-none d-md-block\">\n            <div class=\"px-1 px-md-4\">\n                <h4>Interfaces settings</h4>\n            </div>\n        </div>\n        <div class=\"nav flex-column bordered pt-0 mt-0 mt-md-4\" id=\"v-tab\" role=\"tablist\" aria-orientation=\"vertical\">\n          <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex active show\" id=\"panelServices-tab\" data-toggle=\"pill\" href=\"#panelServices\" role=\"tab\" aria-controls=\"panelServices\" aria-selected=\"false\">\n              <i class=\"fas fa-share-alt my-auto\"></i><span class=\"d-none d-md-block pl-3\">Interfaces</span>\n          </a>\n          <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelNotifications-tab\" data-toggle=\"pill\" href=\"#panelNotifications\" role=\"tab\" aria-controls=\"panelNotifications\" aria-selected=\"false\">\n              <i class=\"fas fa-bell my-auto\"></i><span class=\"d-none d-md-block pl-3\">Notifications</span>\n          </a>\n        </div>\n        <a class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0\" id=\"save-config\" href=\"#\" role=\"tab\" aria-selected=\"false\" update-url=\"{{ url_for('interface_config') }}\">\n            <i class=\"fas fa-save my-auto\"></i><span class=\"d-none d-md-block pl-2\">Save</span>\n        </a>\n        <a class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0\" id=\"reset-config\" href=\"#\" role=\"tab\" aria-selected=\"false\">\n            <i class=\"fas fa-redo-alt my-auto\"></i><span class=\"d-none d-md-block pl-2\">Reset all</span>\n        </a>\n        <button class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0 mt-3 mt-mb-5 mb-5\" id=\"save-config-and-restart\" href=\"#\" role=\"tab\" type=\"button\" aria-selected=\"false\" update-url=\"{{ url_for('interface_config') }}\">\n            <i class=\"fas fa-power-off my-auto\"></i><span class=\"d-none d-md-block pl-2\">Apply changes and restart</span>\n        </button>\n        {% if current_user.is_authenticated %}\n            <a href=\"{{ url_for('logout') }}\" class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-secondary waves-effect d-flex mx-0 mt-3 mt-mb-5 mb-5\">\n                <i class=\"fas fa-sign-out-alt my-auto\"></i><span class=\"d-none d-md-block pl-2\">Lock web interface</span>\n\n            </a>\n        {% endif %}\n    </div>\n  </nav>\n  <main role=\"main\" class=\"col-md-9 col-lg-10 col-11 ml-auto px-4\">\n    <div class=\"tab-content\" id=\"super-container\">\n      <div class=\"tab-pane fade config-root show active\" id=\"panelServices\" role=\"tabpanel\" aria-labelledby=\"panelServices-tab\">\n          <div class=\"card\">\n            <div class=\"card-header\"><h2>Interfaces</h2></div>\n            <div class=\"card-body deck-container\">\n                <div class=\"card\">\n                    <div class=\"card-body\">Select an interface :\n                    <select id=\"AddServiceSelect\" class=\"selectpicker\" data-live-search=\"true\">\n                       {% for service in services_list | sort() %}\n                            <option data-tokens=\"{{ service }}\">{{ service }}</option>\n                       {% endfor %}\n                    </select>\n                    <button type=\"button\" id=\"AddService\" class=\"btn btn-primary add-btn px-3 waves-effect\"><i class=\"fa fa-plus pr-2\" aria-hidden=\"true\"></i> Add</button>\n                    </div>\n                </div>\n                <br>\n                <!-- Card deck -->\n                <div class=\"card-deck config-container\" update-url=\"{{ url_for('interface_config') }}\">\n                    {% for service in services_list %}\n                        {% if service in config_services %}\n                            {{ m_config_service_card.config_service_card(config_services, service, services_list[service], extension_name=OCTOBOT_EXTENSION_PACKAGE_1_NAME) }}\n                        {% endif %}\n                    {% endfor %}\n                </div>\n            </div>\n        </div>\n      </div>\n      <div class=\"tab-pane fade config-root\" id=\"panelNotifications\" role=\"tabpanel\" aria-labelledby=\"panelNotifications-tab\">\n          <div class=\"card\">\n            <div class=\"card-header\">\n                <h2>\n                    Notifications\n                    <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-configuration/accounts#notifications\">\n                        <i class=\"fa-solid fa-question\"></i>\n                    </a>\n                </h2>\n            </div>\n            <div class=\"card-body deck-container\">\n                <!-- Card deck -->\n                <div class=\"card-deck config-container\">\n                    {{ m_config_notification.config_notification(config_notifications, \"notification\", notifiers_list) }}\n                </div>\n            </div>\n        </div>\n      </div>\n    </div>\n  </main>\n</div>\n\n<!-- Default cards -->\n<div class=\"d-none\">\n    <!-- Services -->\n    <div id=\"AddService-template-default\">\n        {% for service in services_list %}\n            <div id=\"AddService-template-default-{{service}}\">\n            {{ m_config_service_card.config_service_card(  config_services,\n                                                           service,\n                                                           services_list[service],\n                                                           add_class=added_class,\n                                                           no_select=True,\n                                                           default_values=True,\n                                                           extension_name=OCTOBOT_EXTENSION_PACKAGE_1_NAME) }}\n            </div>\n        {% endfor %}\n    </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/exchange_accounts.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/navbar.html",
    "content": "<!-- Navbar -->\n<nav class=\"navbar navbar-expand-lg sticky-top py-0 py-md-2\" id=\"main-nav-bar\">\n    <div class=\"navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2\">\n        <ul class=\"navbar-nav mr-auto\" id=\"main-nav-left-part\">\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'home' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('home') }}\">Dashboard</a>\n            </li>\n            <li id=\"main-nav-portfolio\" class=\"nav-item mx-1 px-0 my-auto {% if 'portfolio' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('portfolio') }}\">Portfolio</a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'configuration' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('configuration') }}\">Configuration</a>\n            </li>\n        </ul>\n    </div>\n    <div class=\"d-flex mx-auto order-0\">\n        <a class=\"navbar-brand mx-auto font-weight-bolder\" href=\"{{ url_for('home') }}\">\n            <img\n                src=\"{{url_for('static', filename='img/octobot-logo-'+get_color_mode()+'.png')}}\" alt=\"octobot-logo\"\n                class=\"navbar-logo\"\n            > OctoBot Market Making\n            <i id=\"navbar-bot-status\" class=\"ml-2 fa fa-check\" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"OctoBot operational\"></i>\n        </a>\n        <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".dual-collapse2\">\n            <span class=\"navbar-toggler-icon\"></span>\n        </button>\n    </div>\n    <div class=\"navbar-collapse collapse w-100 order-3 dual-collapse2\">\n        <ul class=\"navbar-nav ml-auto\" id=\"main-nav-right-part\">\n            {% if (not is_login_required()) or (is_login_required() and is_authenticated()) %}\n            <li class=\" mx-1 px-0 my-auto\" id=\"trading-type-indicator\">\n                <span class=\"p-2\" aria-label=\"TradingSwitch\">\n                    {{ m_trading_state.display_trading_state(is_real_trading(get_current_profile()), get_enabled_trader(get_current_profile())) }}\n                </span>\n            </li>\n            {% endif %}\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'cloud' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('cloud') }}\">Cloud platform</a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'logs' == active_page %} active{% endif %}\">\n                <a class=\"nav-link\" href=\"{{ url_for('logs') }}\" aria-label=\"Logs\"><i class=\"fa fa-bell\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Event logs\">\n                    <span id=\"errors-count-badge\" class=\"badge badge-warning\"></span>\n                </i></a>\n            </li>\n            <li class=\"nav-item mx-1 px-0 my-auto {% if 'logs' == active_page %} active{% endif %}\">\n                <a id=\"theme-switch\" class=\"nav-link\" href=\"#\" aria-label=\"Switch theme\" data-update-url=\"{{url_for('api.display_config')}}\">\n                    <i class=\"{{'fa fa-moon' if get_color_mode() == 'light' else 'fas fa-sun'}}\" data-toggle=\"tooltip\" data-placement=\"top\"\n                       title=\"Use {{'dark' if get_color_mode() == 'light' else 'light'}} theme\">\n                </i></a>\n            </li>\n        </ul>\n    </div>\n</nav>\n<!-- Navbar -->"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/distributions/market_making/portfolio.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"portfolio\" %}\n{% import 'macros/cards.html' as m_cards %}\n{% import 'macros/starting_waiter.html' as m_waiter %}\n{% block body %}\n\n<div id=\"portfolio-display\">\n    {% macro display_init_warning() -%}\n        {% if initializing_currencies_prices %}\n        <div class=\"alert alert-warning\" role=\"alert\">\n            OctoBot is currently initializing prices for {{ initializing_currencies_prices | join(\", \") }}.\n            These assets might take a few seconds to load.\n        </div>\n        {% endif %}\n    {%- endmacro %}\n\n    {% macro holding_row(holdings, holding_type) -%}\n            <td class=\"align-middle rounded-number\" data-toggle=\"tooltip\" title=\"{{get_exchange_holdings(holdings, holding_type)}}\">\n                {{holdings[holding_type]}}\n            </td>\n    {%- endmacro %}\n\n    {% macro portfolio_holding(currency, holdings, value) -%}\n        <tr class=\"symbol-holding text-center\">\n            <td class=\"row mx-0\">\n                <div class=\"col col-md-5 animated px-2 fadeIn img-fluid very-small-size\">\n                    <img class=\"card-img-top currency-image\"\n                         src=\"{{ url_for('static', filename='img/svg/loading_currency.svg') }}\"\n                         alt=\"{{currency}}\"\n                         data-symbol=\"{{currency.lower()}}\">\n                </div>\n                <div class=\"d-none d-md-inline col-7 my-auto\">\n                    <span class=\"symbol\">{{currency}}</span></div>\n            </td>\n            {{ holding_row(holdings, \"total\") }}\n            <td class=\"total-value align-middle rounded-number\">{{value}}</td>\n            {{ holding_row(holdings, \"free\") }}\n            {{ holding_row(holdings, \"locked\") }}\n        </tr>\n    {%- endmacro %}\n\n    <br>\n    {% if not has_real_trader and not has_simulated_trader %}\n        {{ m_waiter.display_loading_message(details=\"If this message remains, please make sure that at least one exchange is enabled in your profile.\") }}\n    {% else %}\n        <div class=\"card\" id=\"portfoliosCard\" reference_market=\"{{reference_unit}}\">\n            {{ display_init_warning() }}\n            <div class=\"card-header\"><h2>Portfolio: <span class=\"rounded-number\">{{displayed_portfolio_value}}</span> {{reference_unit}}</h2></div>\n            <div class=\"card-body row mx-0 justify-content-center\">\n                {% if displayed_portfolio %}\n                    <div class=\"col-12 col-md-6 mb-2 mb-md-4\" id=\"portfolio_doughnutChart\"\n                         data-md-height=\"350\" data-sm-height=\"200\">\n                    </div>\n                    <div class=\"col-12\">\n                        <table class=\"table table-striped table-responsive-sm\" id=\"holdings-table\">\n                          <thead>\n                            <tr class=\"text-center\">\n                                <th scope=\"col\">Asset</th>\n                                <th scope=\"col\">Total</th>\n                                <th scope=\"col\">Value in {{reference_unit}}</th>\n                                <th scope=\"col\">Available</th>\n                                <th scope=\"col\">Locked in orders</th>\n                            </tr>\n                          </thead>\n                          <tbody>\n                            {% for currency, holdings in displayed_portfolio.items() %}\n                                {{ portfolio_holding(currency, holdings, symbols_values[currency]) }}\n                            {% endfor %}\n                          </tbody>\n                        </table>\n                    </div>\n                {% else %}\n                    <div class=\"card-subtitle\">\n                        <h2 class=\"text-muted\">Nothing there.</h2>\n                        <p>\n                            If a trader is enabled, please check <a href=\"{{url_for('logs')}}\">your OctoBot logs</a>.\n                            There might be an issue with your exchange credentials.\n                        </p>\n                    </div>\n                {% endif %}\n            </div>\n            <div class=\"card-footer d-flex justify-content-end\">\n                <div class=\"d-flex justify-content-end\">\n                    <button\n                        data-url=\"{{ url_for('api.clear_portfolio_history') }}\"\n                        id=\"clear-portfolio-history-button\"\n                        class=\"btn btn-outline-warning waves-effect\"\n                        data-toggle=\"tooltip\"\n                        data-placement=\"top\"\n                        title=\"Reset portfolio and profitability historical values.\"\n                    >\n                        <i class=\"fas fa-trash\"></i> Reset history\n                    </button>\n                    {% if has_real_trader%}\n                    <button id=\"refresh-portfolio\" update-url=\"{{ url_for('api.refresh_portfolio') }}\"\n                            class=\"btn btn-outline-danger btn-lg waves-effect\"\n                            data-toggle=\"tooltip\"\n                            data-placement=\"top\"\n                            title=\"Triggers a total portfolios re-synchronization using exchanges as a reference.\"\n                    >\n                        <i class=\"fa fa-sync\"></i> Force refresh\n                    </button>\n                    {% endif %}\n                </div>\n            </div>\n        </div>\n    {% endif %}\n    <br>\n    {% endblock %}\n</div>\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/portfolio.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/dsl_help.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n\n\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2 id=\"page-title\">OctoBot DSL Help</h2>\n    </div>\n    <div class=\"card-body\">\n        <div class=\"row\">\n            <p>\n                The OctoBot DSL is a domain-specific language for creating scripts that can be used in the OctoBot platform. It is based on the Python syntax and adds custom functions and keywords to easily create executable expressions.\n            </p>\n            <p>\n                Keywords can be combined to create more complex expressions.\n            </p>\n        </div>\n        <div class=\"row\">\n            <h3>Syntax and examples</h3>\n            <p>\n                The OctoBot DSL syntax is based on the Python syntax, which includes base operators like +, -, *, /, %, ==, !=, >, <, >=, <=, and, or.\n            </p>\n            <div>\n                Examples:\n                <ul>\n                    <li>\"close(\"BTC/USDT\", \"1h\")[-1]\" returns the close price of the last 1h candle of the \"BTC/USDT\" market</li>\n                    <li>\"close(\"BTC/USDT\", \"1h\")[-1] * 2 + 10\" returns the close price of the last 1h candle of the \"BTC/USDT\" market multiplied by 2 and then added to 10</li>\n                    <li>\"ma(close(\"BTC/USDT\", \"1h\"), 12)[-1]\" returns the 12-period moving average of the close prices of the last 1h candles of the \"BTC/USDT\" market</li>\n                    <li>\"100 if close(\"BTC/USDT\", \"1h\")[-1] > open(\"BTC/USDT\", \"1h\")[-1] else ((1 + 2) * 3)\" returns 100 if the close price of the last 1h candle of the \"BTC/USDT\" market is greater than the open price of the last 1h candle of the \"BTC/USDT\" market, otherwise 9</li>\n                </ul>\n            </div>\n        </div>\n        <div class=\"row\">\n            <h3>Keywords</h3>\n            <p>\n                The following keywords are available in the OctoBot DSL:\n            </p>\n            <table class=\"table table-bordered\" id=\"dsl-keywords-table\">\n                <thead>\n                    <tr>\n                        <th>Keyword</th>\n                        <th>Description</th>\n                        <th>Example</th>\n                        <th>Type</th>\n                    </tr>\n                </thead>\n                <tbody id=\"dsl-keywords-table-body\" data-update-url=\"{{ url_for('api.dsl_keywords_docs') }}\">\n                </tbody>\n            </table>\n        </div>\n    </div>\n</div>\n<br>\n\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/dsl_help.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/extensions.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"community\" %}\n{% import \"components/community/tentacle_packages.html\" as m_tentacle_packages %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n\n{% block body %}\n<br>\n\n{{ m_flash_messages.flash_messages() }}\n{{ m_tentacle_packages.pending_tentacles_install_modal(has_owned_packages_to_install) }}\n{{ m_tentacle_packages.waiting_for_owned_packages_to_install_modal(not has_owned_packages_to_install and auto_refresh_packages) }}\n{{ m_tentacle_packages.select_payment_method_modal(OCTOBOT_EXTENSION_PACKAGE_1_NAME) }}\n\n<div class=\"card\">\n    <div class=\"card-header\">\n        <h2>\n            Improve your bot with the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}\n        </h2>\n    </div>\n    <div class=\"card-body\">\n\n        <div class=\"py-5 row\">\n            <div class=\"col\">\n                <p>\n                    The {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} improves your OctoBot\n                    capabilities and simplifies its use.\n                </p>\n                <p>\n                    It adds the <strong>Strategy Designer</strong> to your OctoBot,\n                    greatly <strong>simplifies the TradingView</strong> configuration, improves <strong>crypto basket</strong>\n                    investments much more.\n                </p>\n                <p>\n                    The extension is completely optional and bound to your OctoBot account. This means that updating or\n                    reinstalling your OctoBot will automatically install your extension as long as you are\n                    connected to your OctoBot account.\n                </p>\n            </div>\n            <div class=\"col text-center my-auto\">\n                <div class=\"text-center\">\n                    <h5>${{price}}</h5>\n                </div>\n                <div>\n                    Lifetime improvement of your OctoBot.\n                </div>\n                <div>\n                    No subscription. Pay with crypto or credit card.\n                </div>\n                <div class=\"mt-4\">\n                    {{ m_tentacle_packages.get_package_button(OCTOBOT_EXTENSION_PACKAGE_1_NAME, is_community_authenticated, has_open_source_package) }}\n                </div>\n                {% if is_community_authenticated %}\n                <div class=\"\">\n                    Authenticated as {{current_logged_in_email}}\n                </div>\n                {% endif %}\n            </div>\n        </div>\n        <div class=\"row mt-5\">\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Optimize your own strategies with the Strategy Designer\n                </h3>\n                <p>\n                    The <a target=\"_blank\" rel=\"noopener\"\n                           href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-usage/strategy-designer?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_text\">\n                        Strategy Designer\n                    </a>\n                    is OctoBot's most advanced strategy optimization and backtesting interface.\n                </p>\n                <p>\n                    Using the Strategy Designer, you can:\n                    <ul>\n                        <li>\n                            Compare backtesting results of your strategies\n                        </li>\n                        <li>\n                            Visualize your strategies behavior through time\n                        </li>\n                        <li>\n                            Optimize your strategy while using a different live profile\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-usage/strategy-designer?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n                        Learn more on the Strategy Designer\n                    </a>\n                </div>\n            </div>\n            <div class=\"col-12 col-lg-6 text-center\">\n                <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/V4Z1xUhqWz8?showinfo=0&amp;rel=0\"\n                title=\"The OctoBot Strategy Designer\" frameborder=\"0\" allow=\"accelerometer; autoplay;\n                clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n            </div>\n        </div>\n        <div class=\"row feature-margin\">\n            <div class=\"col-12 col-lg-6\">\n                <img class=\"img-fluid img-feature\"\n                         src=\"{{url_for('static', filename='img/community/tentacles_packages_previews/tradingview-ema-strategy-illustration-with-2-buy-and-2-sell.png')}}\" alt=\"OctoBot cloud\">\n            </div>\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Seamlessly connect your TradingView webhook\n                </h3>\n                <p>\n                    In the default version of OctoBot, advanced technical knowledge or a paid external\n                    webhook provider such as Ngrok is required to connect to your TradingView alerts.\n                <p>\n                </p>\n                    Using OctoBot with the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} makes TradingView strategies automation:\n                    <ul>\n                        <li>\n                            Simple: Use OctoBot cloud webhooks - no Ngrok subscription is needed for webhooks\n                        </li>\n                        <li>\n                            Easy: You get a unique alert email address and webhook URL you can use rightaway\n                        </li>\n                        <li>\n                            Secure: Use the OctoBot cloud secure email and webhook system\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/guides/octobot-trading-modes/tradingview-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n                        Learn more on TradingView Strategies\n                    </a>\n                </div>\n            </div>\n        </div>\n        <div class=\"row feature-margin\">\n            <div class=\"col-12 col-lg-6\">\n                <h3>\n                    Use and configure your improved crypto baskets\n                </h3>\n                <p>\n                    OctoBot cloud <a target=\"_blank\" rel=\"noopener\"\n                                     href=\"{{OCTOBOT_COMMUNITY_URL}}/{{LOCALE}}/features/crypto-basket?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_text\">\n                        crypto baskets\n                    </a>\n                    are special strategies that make it easy to invest in the top crypto of the market or specific themes.\n                </p>\n                <p>\n                    With improved crypto baskets :\n                    <ul>\n                        <li>\n                            Follow automatically updated OctoBot cloud crypto baskets\n                        </li>\n                        <li>\n                            Or start from an existing basket and customize it\n                        </li>\n                        <li>\n                            Watch your portfolio automatically follow the latest trends\n                        </li>\n                    </ul>\n                </p>\n                <div class=\"text-center mt-5\">\n                    <a type=\"button\" class=\"btn btn-outline-primary\" target=\"_blank\" rel=\"noopener\"\n                       href=\"{{OCTOBOT_COMMUNITY_URL}}/features/crypto-basket?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=extensions_button\">\n                        Learn more about crypto baskets\n                    </a>\n                </div>\n            </div>\n            <div class=\"col-12 col-lg-6\">\n                <img class=\"img-fluid img-feature\"\n                             src=\"{{url_for('static', filename='img/community/tentacles_packages_previews/crypto-basket.png')}}\" alt=\"OctoBot cloud\">\n            </div>\n        </div>\n        <div class=\"feature-margin d-flex justify-content-center\">\n            <div class=\"mx-auto\">\n                <h3 class=\"text-center\">\n                    And more!\n                </h3>\n                <p class=\"text-center\">\n                    With the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} :\n                </p>\n\n                <p class=\"\">\n                    <ul>\n                        <li>\n                            Invest using exclusive strategies and profiles\n                        </li>\n                        <li>\n                            Join the exclusive Discord channel\n                        </li>\n                    </ul>\n                </p>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"py-4 text-center\">\n        <div>\n            {{ m_tentacle_packages.get_package_button(OCTOBOT_EXTENSION_PACKAGE_1_NAME, is_community_authenticated, has_open_source_package) }}\n        </div>\n        <div class=\"mt-5\">\n            Any question on the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}} ? Just\n            <a href=\"mailto:contact@octobot.cloud?subject=About the {{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}\">ask the team</a>.\n        </div>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/extensions.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/index.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"home\" %}\n\n{% import 'macros/critical_notifications_alert.html' as m_critical_notifications %}\n\n{% block body %}\n    <br>\n    <div update-url=\"{{ url_for('api.announcements') }}\" class=\"alert alert-danger text-center d-none\" role=\"alert\" id=\"annoncementsAlert\"></div>\n\n    {% if display_ph_launch %}\n    <div class=\"alert alert-primary text-center\" role=\"alert\">\n        <h5 class=\"\">\n            🎉 Major news! OctoBot is launching on Product Hunt {{'today' if is_launching else 'on the 10th of July'}}.\n            {% if not is_launching %}\n            <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\" data-role=\"hide-announcement\" data-url=\"{{url_for('api.hide_announcement', key='product_hunt_announcement')}}\">\n                <span aria-hidden=\"true\">&times;</span>\n            </button>\n            {% endif %}\n        </h5>\n        <p class=\"text-center pt-3 mb-0\">\n            <a href=\"https://www.producthunt.com/posts/octobot-open-source?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-octobot&#0045;open&#0045;source\" target=\"_blank\"><img src=\"https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=463219&theme={{get_color_mode()}}\" alt=\"OctoBot&#0032;open&#0032;source - Your&#0032;open&#0032;source&#0032;investment&#0032;strategy&#0032;builder | Product Hunt\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" /></a>\n\n        </p>\n        <p class=\"text-center pt-3 mb-0\">\n            Follow the launch to get your exclusive discount.\n        </p>\n    </div>\n    {% endif %}\n    {% if not IS_CLOUD %}\n    <div class=\"d-none alert alert-success text-center my-2\" role=\"alert\">\n        <h5 class=\"d-none d-sm-inline\">\n            <span class=\"d-none d-md-inline\"><i class=\"far fa-bell\"></i> Good news ! </span>\n            OctoBot version <span update-url=\"{{ url_for('api.upgrade_version') }}\" id=\"upgradeVersion\"></span>\n            is available.\n        </h5>\n        <button route=\"{{ url_for('commands', cmd='update') }}\" type=\"button\" class=\"btn btn-outline-primary waves-effect\">\n            Upgrade now <i class=\"fas fa-cloud-download-alt\"></i>\n        </button>\n    </div>\n    {% endif %}\n    <span id=\"exchange-specific-data\">\n        {% if display_trading_delay_info %}\n            <div class=\"alert alert-info mt-2\" role=\"alert\">\n                <i class=\"fa-regular fa-lightbulb\"></i>\n                A new trading mode has just been selected, OctoBot is now looking for trading opportunities.\n                Depending on the profile settings, a few hours might be required before the first orders are created.\n                Use <a href=\"backtesting\">Backesting</a> to quickly test trading strategies.\n            </div>\n        {% endif %}\n        {% if is_in_stating_community_env() %}\n        <div class=\"card my-2 my-md-4\">\n            <div class=\"card-header\"><h2>\n                Welcome to the OctoBot beta environment\n                </h2>\n            </div>\n            <div class=\"card-body card-text\">\n                <div class=\"alert alert-info\">\n                    When the beta environment is enabled, you will be connected to the \"in development\"\n                    version of OctoBot cloud (<a href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=beta_program&utm_content=dashboard\">{{OCTOBOT_COMMUNITY_URL}})</a>.\n                    The beta OctoBot cloud has its own accounts and products.\n                    Please login using your beta account.\n                </div>\n            </div>\n            <div class=\"card-footer\">\n                You can disable the beta environment and go back to the regular one at anytime from the\n                <a href=\"{{ url_for('about', _anchor='beta-program') }}\"> about tab</a>.\n            </div>\n        </div>\n        {% endif %}\n        {{ m_critical_notifications.critical_notifications_alert(critical_notifications) }}\n        <div class=\"card\" id=\"profitability-display\">\n            <div class=\"card-header\">\n                <h4>\n                    <div class=\"row\">\n                        <div class=\"d-none d-md-flex col-md-3\">\n                            Portfolio value\n                        </div>\n                        <div class=\"col-10 col-md-6 text-md-center\">\n                            <span class=\"d-none align-middle\"\n                                  id=\"flat-profitability\">\n                                <span id=\"flat-profitability-text\">\n                                </span>\n                                {{reference_unit}}\n                            </span>\n                            <span class=\"badge d-none\"\n                                  id=\"profitability-badge\"\n                                  data-toggle=\"tooltip\"\n                                  title=\"Portfolio total value change since the first valuation. You can reset it from the portfolio tab.\"\n                            ><span id=\"profitability-value\"></span>%</span>\n                        </div>\n                        <div class=\"col-2 offset-md-1 text-right px-0\">\n                            <a class=\"blue-text\"\n                               href=\"{{url_for('trading', _anchor='panel-pnl')}}\"\n                               data-toggle=\"tooltip\"\n                               title=\"{{ 'Profit and Loss' if has_pnl_history else 'Profit and Loss: requires trading with a PNL compatible trading mode.'}}\">\n                                <i class=\"fa-solid fa-chart-line\"></i>\n                            </a>\n                            <a class=\"blue-text\"\n                               href=\"{{url_for('portfolio')}}\"\n                               data-toggle=\"tooltip\"\n                               title=\"Portfolio details\">\n                                <i class=\"fas fa-chart-pie\"></i>\n                            </a>\n                        </div>\n                    </div>\n                </h4>\n            </div>\n            <div class=\"card-body card-text\" id=\"profitability_graph\">\n                <div class=\"w-100\">\n                    <div id=\"portfolio_historyChart\"\n                         data-url=\"{{url_for('api.historical_portfolio_value', currency=reference_unit, time_frame='')}}\"\n                         data-reference-market=\"{{reference_unit}}\"\n                         class=\"w-100\">\n                    </div>\n                </div>\n            </div>\n            <div class=\"card-body card-text d-none\" id=\"no_profitability_graph\">\n                Your daily portfolio value history will be displayed here.\n            </div>\n        </div>\n        <br>\n        <div class=\"card\">\n            <div class=\"card-header d-flex justify-content-between\" id=\"all-watched-markets\">\n                <div>\n                    {% if sandbox_exchanges %}\n                    <h5>\n                        <span class=\"badge badge-warning\"\n                              id=\"sandbox-badge\"\n                              data-toggle=\"tooltip\"\n                              title=\"{{ sandbox_exchanges | join(' and ')}} testnet / sandbox is enabled. This means that\n                              your OctoBot is trading using testnet prices and accounts, which\n                              might not be representative of real markets. Use the trading simulator to test a trading\n                              strategy in real conditions.\"\n                        >{{sandbox_exchanges[0] | capitalize}} sandbox</span>\n                    </h5>\n                    {% endif %}\n                </div>\n                <h4>Watched markets</h4>\n                <div>\n                    <a class=\"waves-effect float-right\" href=\"#\" id=\"display-dashboard-settings-modal-label\"\n                       data-toggle=\"modal\" data-target=\"#dashboard-settings-modal\">\n                       <i class=\"fa fa-1_5x fa-cog\"></i>\n                    </a>\n                </div>\n            </div>\n            <div class=\"card-body d-none text-center\" id=\"loadingMarketsDiv\">\n                <h2>\n                    OctoBot is starting, markets will be refreshed when exchanges will be reachable.\n                </h2>\n            </div>\n            <div class=\"card-body text-center\" name=\"loadingSpinner\">\n                <h2>\n                    <i class=\"fa fa-spinner fa-spin\"></i>\n                </h2>\n                <p class=\"mt-5\">\n                    If this loader remains, please make sure that at least one exchange is enabled in\n                    <a href=\"{{url_for('profile', _anchor='panelExchanges')}}\">your profile</a>.\n                </p>\n            </div>\n            {% if backtesting_mode %}\n                <div class=\"card-body\" id=\"first_symbol_graph\" update-url=\"{{ url_for('first_symbol') }}\" backtesting_mode={{backtesting_mode}}>\n                    <div id=\"graph-symbol-price\"></div>\n                </div>\n            {% else %}\n                {% for pair in watched_symbols %}\n                    <div class=\"card-body candle-graph\" id=\"{{pair}}_graph\" backtesting_mode={{backtesting_mode}}>\n                        <div class=\"watched-symbol-graph\" id=\"{{pair}}_graph-symbol-price\" symbol=\"{{pair}}\"></div>\n                    </div>\n                {% endfor %}\n                <div class=\"card-body candle-graph d-none\" id=\"first_symbol_graph\" update-url=\"{{ url_for('first_symbol') }}\" backtesting_mode={{backtesting_mode}}>\n                    <div id=\"graph-symbol-price\"></div>\n                </div>\n                {% if not watched_symbols %}\n                    <div class=\"card-footer\">No watched markets: using a default one. You can add\n                        <a href=\"#\"><i class=\"far fa-star\" aria-label=\"Watched markets star\"></i></a>\n                        watched markets in the <a href=\"{{ url_for('trading', _anchor='panel-market-status') }}\">trading section</a>.\n                    </div>\n                {% endif %}\n            {% endif %}\n        </div>\n        <div class=\"modal\" id=\"dashboard-settings-modal\" tabindex=\"-1\" role=\"dialog\"\n             aria-labelledby=\"#display-dashboard-settings-modal-label\" aria-hidden=\"true\">\n          <div class=\"modal-dialog modal-dialog-centered modal-md\" role=\"document\">\n            <div class=\"modal-content modal-text\">\n              <div class=\"modal-header primary-text\">\n                <h2 class=\"modal-title\">Settings</h2>\n                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n                      <span aria-hidden=\"true\">&times;</span>\n                    </button>\n              </div>\n              <div class=\"modal-body\">\n                  <div class=\"mx-4\">\n                    <label for=\"timeFrameSelect\">Dashboard time frame</label>\n                    <select class=\"selectpicker\" id=\"timeFrameSelect\" data-live-search=\"true\" data-update-url=\"{{url_for('api.display_config')}}\">\n                      {% for time_frame in all_time_frames %}\n                        <option value=\"{{time_frame.value}}\" {{'selected=\"selected\"' if time_frame.value == display_time_frame}}>\n                            {{time_frame.value}}\n                        </option>\n                      {% endfor %}\n                    </select>\n                    <div class=\"custom-control custom-switch my-auto\" id=\"synchronized-data-only-div\">\n                        <input type=\"checkbox\" class=\"custom-control-input\" id=\"displayOrderToggle\" {{'checked' if display_orders}}  data-update-url=\"{{url_for('api.display_config')}}\">\n                        <label class=\"custom-control-label\" for=\"displayOrderToggle\">Display orders</label>\n                    </div>\n                    <div class=\"font-italic\">Add and remove watched symbols from the <a href=\"{{ url_for('trading', _anchor='panel-market-status') }}\">trading tab</a>.</div>\n                  </div>\n              </div>\n            </div>\n          </div>\n        </div>\n    </span>\n    <br>\n    <span class=\"d-none\"\n          data-display-intro=\"{{display_intro}}\"\n          data-selected-profile=\"{{selected_profile}}\"\n    ></span>\n    <span class=\"d-none\" id=\"feedback-form-data\"\n          data-display-form=\"{{display_feedback_form}}\"\n          data-user-id=\"{{user_id}}\"\n          data-form-to-display=\"{{form_to_display}}\"\n          data-on-submit-url=\"{{url_for('api.register_submitted_form')}}\"\n    ></span>\n{% endblock %}\n\n{% block additional_scripts %}\n    <script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/common/candlesticks.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/common/portfolio_history.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/components/dashboard.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/components/dashboard_tutorial_starter.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n    <script src=\"{{ url_for('static', filename='js/components/tentacles_configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/layout.html",
    "content": "{% import 'macros/trading_state.html' as m_trading_state %}\n{% import 'components/modals/trading_state_modal.html' as m_trading_state_modal %}\n{% import 'components/modals/generic_modal.html' as m_generic_modal %}\n{% import 'components/community/user_details.html' as m_user_details %}\n<!doctype html>\n<html lang=\"en\" data-mdb-theme=\"{{get_color_mode()}}\">\n    {% set active_page = active_page|default('home') -%}\n    {% set page_title = page_title|default(active_page | replace(\"_\", \" \") | capitalize) -%}\n    {% set startup_messages_added_classes = startup_messages_added_classes|default('') -%}\n    {% set inner_startup_messages_added_classes = inner_startup_messages_added_classes|default('col-12') -%}\n\n    <head>\n        <title>{{ page_title }} - OctoBot</title>\n\n        <!-- Required meta tags -->\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n        <!-- Prevent search engines indexing -->\n        <meta name=\"robots\" content=\"noindex\">\n\n        <!-- Favicon -->\n        <link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.png') }}\">\n\n        {% block additional_meta %}\n        {% endblock additional_meta %}\n\n        <link rel=\"stylesheet\" href=\"https://code.jquery.com/ui/1.13.0/themes/ui-lightness/jquery-ui.css\" integrity=\"sha384-XgQGwtMpBAzdVt3enHfJp3btU0JUUlr8SkglEJbUTcNIiIBj9fovNuSxOMZLhete\" crossorigin=\"anonymous\">\n        <!-- Bootstrap CSS -->\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css\" integrity=\"sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N\" crossorigin=\"anonymous\">\n\n        <!-- Fontawesome CSS -->\n        <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v6.4.0/css/all.css\" integrity=\"sha384-iw3OoTErCYJJB9mCa8LNS2hbsQ7M3C0EpIsO/H5+EGAkPGc6rk+V8i04oW/K5xq0\" crossorigin=\"anonymous\">\n\n        <!-- toaster -->\n        <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\" integrity=\"sha384-YzEqZ2pBV0i9OmlTyoz75PqwTR8If8GsXBv7HLQclEVqIC3VxIt98/U94ES6CJTR\" crossorigin=\"anonymous\">\n\n        <!-- mdb CSS -->\n        <link href=\"https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/7.3.2/mdb.min.css\" integrity=\"kj1RBJ7aqGUnavWQDbYyovF5HQGHlvNf6SZ2CfaCNkoBJBEux2JXFCXqGZTAYENh\" rel=\"stylesheet\" crossorigin=\"anonymous\">\n\n        <!-- Datatables -->\n        <link rel=\"stylesheet\" href=\"https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.min.css\" integrity=\"sha384-zUxWDVAcow8yNu+q4VFsyZA3qWsKKGdWPW0SVjaR12LQze4SY8Nr75US6VDhbWkf\" crossorigin=\"anonymous\">\n\n        <!-- Select -->\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/css/bootstrap-select.min.css\" integrity=\"sha384-2SvkxRa9G/GlZMyFexHk+WN9p0n2T+r38dvBmw5l2/J3gjUcxs9R1GwKs0seeSh3\" crossorigin=\"anonymous\">\n\n        <!-- Editable -->\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/bootstrap-editable.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css\" integrity=\"sha384-OXVF05DQEe311p6ohU11NwlnX08FzMCsyoXzGOaL+83dKAb3qS17yZJxESl8YrJQ\" crossorigin=\"anonymous\">\n\n        <!-- W2UI -->\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/w2ui-dark-1.5.min.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n\n        <!-- introjs-->\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/intro.js@7.0.1/introjs.min.css\" integrity=\"sha384-Vck4FJIhIat27gWVBM++aKmJWSO9FeA7Gl7Zbo+ZeZgUtnv2YcHf9HPFZ4CSIeFc\" crossorigin=\"anonymous\">\n        <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/intro.js@7.0.1/themes/introjs-modern.css\" integrity=\"sha384-RP5n9cz00mKCFIzcN8B7dK41QSmmpc7Gtz1zxZeiaP+lxtpLXtCTYikHs9L/p2kU\" crossorigin=\"anonymous\">\n        <!-- Own -->\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/style.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n        <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/layout.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n\n        {% block additional_style %}\n        {% endblock additional_style %}\n    </head>\n    <body style=\"{% block body_style %}{% endblock body_style %}\">\n        <!-- Scripts -->\n        <!-- At the beginning of the page : be available for template scripts -->\n        <script src=\"https://code.jquery.com/jquery-3.6.4.min.js\" integrity=\"sha384-UG8ao2jwOWB7/oDdObZc6ItJmwUkR/PfMyt9Qs5AwX7PsnYn1CRKCTWyncPTWvaS\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://code.jquery.com/ui/1.13.0/jquery-ui.min.js\" integrity=\"sha384-GH7wmqAxDa43XGS89eXGbziWEki6l/Smy1U+dAI7ZbxlrLsmal+hLlTMqoPIIg1V\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.6.1/socket.io.min.js\" integrity=\"sha384-KA7m0DwgQGmeRC6Xre3hJO+ZxpanOauVh4Czdqbg8lDKJ3bZZYVYmP+y4F31x40L\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js\" integrity=\"sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\" integrity=\"sha384-VDls8ImYGI8SwVxpmjX2Bn27U2TcNodzTNROTusVEWO55+lmL+H9NczoQJk6mwZR\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/7.3.2/mdb.umd.min.js\" integrity=\"sha384-TGRlbFTmiVIUuSy+b/aj9mHaUTABC3gid02pJimnu14vfLMvOzODXgRmw03nf7vs\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js\" integrity=\"sha384-GP2+CwBlakZSDJUr+E4JvbxpM75i1i8+RKkieQxzuyDZLG+5105E1OfHIjzcXyWH\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.plot.ly/plotly-2.20.0.min.js\" integrity=\"sha384-lqNbLAc8irUVsiXijo8d5LY0Ecc43bEe85kyAJgdi+CAvmBPO/L1SWp6EUxChKM/\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/@json-editor/json-editor@2.15.2/dist/jsoneditor.min.js\" integrity=\"sha384-S23hgqTTna4/wcF/J6BYSROaiWWUKzqmjPJ9Obp6aVkBmXG1YQ/MpoiTvnR5yC8h\" crossorigin=\"anonymous\"></script>        <script src=\"https://cdn.datatables.net/2.0.8/js/dataTables.min.js\" integrity=\"sha384-nJy9D0UBD2LV93ED7IXSsdWfa9PumZvn70zRSR/oFw5Zq0x6gWwWdpLeGsbVATVg\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/js/bootstrap-select.min.js\" integrity=\"sha384-SfMwgGnc3UiUUZF50PsPetXLqH2HSl/FmkMW/Ja3N2WaJ/fHLbCHPUsXzzrM6aet\"  crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js\" integrity=\"sha384-d3UHjPdzJkZuk5H3qKYMLRyWLAQBJbby2yr2Q58hXXtAGF8RSNO9jpLDlKKPv5v3\" crossorigin=\"anonymous\"></script>\n        <script src=\"https://cdn.jsdelivr.net/npm/intro.js@7.0.1/intro.min.js\" integrity=\"sha384-Du1qtHnjTA8tiFynVHiYYcUtxaykXTAU+GJCsSDTkZLHC+yYQHSIBDMUbtefOu1M\" crossorigin=\"anonymous\"></script>\n        <script src=\"{{ url_for('static', filename='js/lib/bootstrap-editable.min.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        {# docs in http://w2ui.com/web/demos/#/grid/1 #}\n        <script src=\"{{ url_for('static', filename='js/lib/w2ui-1.5.min.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/cst.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/json_editor_settings.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/util.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/bot_connection.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/dom_updater.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/required.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/tutorial.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"{{ url_for('static', filename='js/common/feedback.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        <script src=\"https://tally.so/widgets/embed.js\"></script>\n        <script src=\"{{ url_for('static', filename='js/components/navbar.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        {{ m_user_details.posthog(IS_DEMO, IS_CLOUD, IS_ALLOWING_TRACKING, PH_TRACKING_ID) }}\n\n        {% set show_nab_bar = show_nab_bar|default(True) -%}\n\n        {% if show_nab_bar %}\n\n        {% if get_distribution() == 'market_making' %}\n            {% include \"distributions/market_making/navbar.html\" %}\n        {% else %}\n            {% include \"distributions/default/navbar.html\" %}\n        {% endif %}\n\n        {% endif %}\n\n        <div class=\"container-fluid\">\n            <noscript>\n                <br>\n                <div class=\"alert alert-warning\" role=\"alert\">\n                    <h2>Javascript is disabled</h2>\n                    <p>Your browser doesn't allow javascript to be executed. To use the OctoBot web interface, please enable javascript and reload this page.</p>\n                </div>\n            </noscript>\n\n            {% if startup_messages %}\n            <div class=\"row text-center {{startup_messages_added_classes}}\">\n                <div class=\"alert alert-warning w-100 {{inner_startup_messages_added_classes}} mt-2 mb-0 my-md-4\">\n                    <a class=\"d-block d-md-none\"\n                       id=\"startup-messages-collapse-control\"\n                       data-toggle=\"collapse\" href=\"#startup-messages-collapse\"\n                       aria-expanded=\"false\" aria-controls=\"collapseOne\">\n                        <div role=\"tab\" id=\"startup-messages-heading\">\n                          <h5 class=\"mb-0\">\n                                {{startup_messages | length }} important message{{'s' if startup_messages | length > 1 else '' }}\n                          </h5>\n                        </div>\n                    </a>\n\n                    <div id=\"startup-messages-collapse\" class=\"collapse\" role=\"tabpanel\" aria-labelledby=\"startup-messages-heading\">\n                      <div>\n                        {% for startup_message in startup_messages %}\n                            <div>\n                                {{ startup_message }}\n                            </div>\n                        {% endfor %}\n                       </div>\n                    </div>\n                </div>\n            </div>\n            {% endif %}\n\n            {% block body %}{% endblock %}\n        </div>\n\n        <!-- urls to read in js -->\n        <span class=\"d-none\" id=\"global-urls\"\n              data-website-url=\"{{OCTOBOT_WEBSITE_URL}}\"\n              data-docs-url=\"{{OCTOBOT_DOCS_URL}}\"\n              data-exchanges-docs-url=\"{{EXCHANGES_DOCS_URL}}\"\n        ></span>\n\n        <!-- Modals -->\n        {{ m_trading_state_modal.create_trading_state_modal(is_real_trading(get_current_profile()), get_enabled_trader(get_current_profile())) }}\n        {{ m_generic_modal.create_generic_modal() }}\n\n        <!-- Artificial padding to separate footer from the rest of the page -->\n        <div class=\"pb-5\"></div>\n        <div class=\"pb-5\"></div>\n        <!-- Artificial padding  -->\n\n        {% if get_distribution() == 'market_making' %}\n            {% include \"distributions/market_making/footer.html\" %}\n        {% else %}\n            {% include \"distributions/default/footer.html\" %}\n        {% endif %}\n\n        <!-- Resources urls -->\n        <span class=\"d-none\" id=\"resources-urls\"\n              data-audio-media-url=\"{{ url_for('audio_media', name='', _external=True) }}\"\n              data-ping-url=\"{{ url_for('api.ping') }}\"\n        ></span>\n\n        {% block additional_scripts %}\n        {% endblock additional_scripts %}\n        <script src=\"{{ url_for('static', filename='js/common/on_load.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n        {{ m_user_details.user_details(\n            USER_EMAIL,\n            USER_SELECTED_BOT_ID,\n            has_open_source_package,\n            PROFILE_NAME,\n            TRADING_MODE_NAME,\n            EXCHANGE_NAMES,\n            IS_REAL_TRADING\n        ) }}\n    </body>\n</html>\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/login.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"home\" %}\n\n{% from \"macros/forms.html\" import render_field %}\n\n{% block body %}\n<br>\n<div class=\"login_box mx-auto mt-1 mt-xl-5\">\n    <div class=\"card text-center\">\n        <div class=\"card-header\">\n            <h2>\n                Welcome back\n                <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-interfaces/web#protect-your-web-interface\">\n                    <i class=\"fa-solid fa-question\"></i>\n                </a>\n            </h2>\n        </div>\n        <div class=\"card-body\">\n            {% with messages = get_flashed_messages(with_categories=true) %}\n              {% if messages %}\n                    {% for category, message in messages %}\n                    <div class=\"alert alert-{{ 'danger' if category == 'error' else 'success' }}\">\n                        {{ message }}\n                    </div>\n                    {% endfor %}\n              {% endif %}\n            {% endwith %}\n            <h5>\n                {% if is_remote_login %}\n                Please enter your OctoBot account password to access your OctoBot\n                {% else %}\n                Please enter your password to access your OctoBot\n                {% endif %}\n            </h5>\n            <form method=post>\n                <div class=\"my-4\">\n                    {{ form.csrf_token }}\n                    <div>\n                        {{ render_field(form.password, autofocus=true, class=\"form-control mx-auto\", placeholder=\"Password\") }}\n                    </div>\n                    <div class=\"custom-control custom-switch mt-2\">\n                        {{ render_field(form.remember_me, class=\"custom-control-input\") }}\n                        <label class=\"custom-control-label\" for=\"remember_me\">Remember me</label>\n                    </div>\n                </div>\n            <input type=submit value=Login class=\"btn btn-primary waves-effect mt-2\">\n            {% if is_remote_login %}\n            <div class=\"mt-2\">\n                <a href=\"{{ OCTOBOT_COMMUNITY_RECOVER_PASSWORD_URL }}\" class=\"font-weight-bold\">Forgot your password ?</a>\n            </div>\n            {% endif %}\n            </form>\n        </div>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/logs.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"logs\" %}\n{% import 'macros/tables.html' as m_tables %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n\n{% macro extract_logs(logs_list) -%}\n    {% for log in logs_list %}\n        {{ m_tables.logs_tr(log) }}\n    {% endfor %}\n{%- endmacro %}\n\n{% macro extract_notifications(notifications_list) -%}\n    {% for notification in notifications_list %}\n        {{ m_tables.notifications_tr(notification) }}\n    {% endfor %}\n{%- endmacro %}\n\n{% block body %}\n<br>\n<div class=\"card\">\n    <div class=\"card-header\"><h2>Logs & Notifications</h2></div>\n    <div class=\"card-body\">\n        {{ m_flash_messages.flash_messages() }}\n        <div>\n            <ul class=\"nav nav-tabs md-tabs justify-content-center\" id=\"tabs\" role=\"tablist\">\n                <li class=\"nav-item\">\n                    <a class=\"nav-link primary-tab-selector active show\" id=\"logs-tab\" data-toggle=\"tab\" href=\"#logs\" role=\"tab\"\n                       aria-controls=\"logs\"\n                       aria-selected=\"true\">\n                        <h5>Logs</h5>\n                    </a>\n                </li>\n                <li class=\"nav-item\">\n                    <a class=\"nav-link primary-tab-selector\" id=\"notifications-tab\" data-toggle=\"tab\" href=\"#notifications\" role=\"tab\"\n                       aria-controls=\"notifications\"\n                       aria-selected=\"false\">\n                        <h5>Notifications</h5>\n                    </a>\n                </li>\n            </ul>\n        </div>\n        <div class=\"tab-content my-2\">\n            <div class=\"tab-pane fade show active\" id=\"logs\" role=\"tablogs\" aria-labelledby=\"logs-tab\">\n              <table id=\"logs_datatable\" class=\"table table-striped table-responsive-sm w-100\">\n                <caption>Find the full current and previous OctoBot executions information in logs/OctoBot.log files.</caption>\n              <thead>\n                <tr>\n                    <th scope=\"col\">Time</th>\n                    <th scope=\"col\">Level</th>\n                    <th scope=\"col\">Source</th>\n                    <th scope=\"col\">Message</th>\n                </tr>\n              </thead>\n                  <tbody>\n                    {{ extract_logs(logs) }}\n                  </tbody>\n              </table>\n              <div class=\"text-center mb-2\">\n                  <button id=\"export-logs\"\n                          data-url=\"{{url_for('export_logs')}}\"\n                          class=\"btn btn-outline-primary waves-effect export-logs-button\"\n                          data-toggle=\"tooltip\" title=\"Export your logs into a zipped file to help fixing the issues you might have.\">\n                      <i class=\"fas fa-share-square\"></i> <span class=\"d-none d-md-inline\">Download detailed logs</span>\n                  </button>\n              </div>\n            </div>\n            <div class=\"tab-pane fade\" id=\"notifications\" role=\"tabnotifications\" aria-labelledby=\"notifications-tab\">\n                <table id=\"notifications_datatable\" class=\"table table-striped table-responsive-sm w-100\">\n                    <caption>History of notifications you enabled as web interface and/or telegram notifications.</caption>\n                    <thead>\n                        <tr>\n                            <th scope=\"col\">Time</th>\n                            <th scope=\"col\">Title</th>\n                            <th scope=\"col\">Message</th>\n                            <th scope=\"col\">Type</th>\n                        </tr>\n                    </thead>\n                    <tbody>\n                       {{ extract_notifications(notifications) }}\n                    </tbody>\n                </table>\n            </div>\n        </div>\n    </div>\n</div>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/logs.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/backtesting_utils.html",
    "content": "{% macro backtesting_report(source, OCTOBOT_DOCS_URL, has_open_source_package) -%}\n\n<span id=\"backtestingReport\" update-url=\"{{ url_for('backtesting', update_type='backtesting_report', source=source) }}\" loading=\"false\" style='display: none;'>\n    <div class=\"card\">\n        <div class=\"card-header\">\n            <h2>\n                Backtesting results\n                <a class=\"float-right badge badge-info waves-effect\" id=\"reportTradingModeNameLink\" href=\"\" base_href=\"{{ url_for('config_tentacle', name='') }}\">\n                    Trading mode: <span id=\"reportTradingModeName\"></span>\n                </a>\n            </h2>\n        </div>\n        <div class=\"card-body\">\n            <div class=\"alert alert-danger\" id=\"backtestingErrorsAlert\" role=\"alert\" style=\"display: none;\">\n              Errors occurred during backtesting, <a href=\"{{ url_for('logs') }}\" class=\"alert-link\">details in logs</a>.\n            </div>\n            <table class=\"table table-striped table-sm table-responsive-lg\">\n              <tbody>\n                <tr>\n                  <td>Bot profitability</td><td id=\"bProf\"></td>\n                </tr>\n                <tr>\n                  <td>Market average profitability</td><td id=\"maProf\"></td>\n                </tr>\n                <tr>\n                  <td>Symbol profitability</td><td id=\"sProf\"></td>\n                </tr>\n                <tr>\n                  <td>End portfolio</td><td id=\"ePort\"></td>\n                </tr>\n                <tr>\n                  <td>Starting portfolio</td><td id=\"sPort\"></td>\n                </tr>\n                <tr>\n                  <td>Reference market</td><td id=\"refM\"></td>\n                </tr>\n              </tbody>\n            </table>\n        </div>\n        {% if not has_open_source_package() %}\n        <div class=\"card-footer\">\n            <i class=\"fa fa-info-circle\"></i> Looking for more in-depth backtesting results? The <a href=\"{{ url_for('extensions') }}\">Strategy designer</a> is specifically designed for advanced backtestings and strategy optimization.\n        </div>\n        {% endif %}\n        <div class=\"card-footer\">\n            <div class=\"text-right\">\n                <label for=\"timeFrameSelect\">Time frame</label>\n                <select class=\"selectpicker\" id=\"timeFrameSelect\" data-live-search=\"true\"></select>\n            </div>\n            <div id=\"result-graphs\">\n            </div>\n            <div>\n                <h4 class=\"text-center\">Trades</h4>\n                <table id=\"result-trades\"\n                     class=\"w-100 table-striped table-responsive-sm\">\n                </table>\n            </div>\n\n            <div class=\"alert mb-0 text-right\">\n                Learn more details on how backtesting works in OctoBot on\n                <a class=\"\" target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-usage/backtesting?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=backtesting_footer\">the backtesting guide</a>.\n            </div>\n        </div>\n\n        <div class=\"card-footer\">\n            <i class=\"fa fa-info-circle\"></i> Backtesting results depend on starting conditions such as\n            <a href=\"{{ url_for('profile')+'#panelEvaluators' }}\">evaluator configuration</a> and the <a href=\"{{ url_for('profile')+'#panelTrading' }}\">\n            starting portfolio</a>.\n\n        </div>\n\n    </div>\n</span>\n<div class=\"d-none\">\n    <!--Symbol graph template -->\n    <div class=\"card result-graph\" id=\"result-graph-Bitcoin\">\n        <div class=\"card-body text-center\" name=\"loadingSpinner\">\n            <h2>\n                <i class=\"fa fa-spinner fa-spin\"></i>\n            </h2>\n        </div>\n        <div class=\"card-body\">\n            <div id=\"graph-symbol-price-Bitcoin\"></div>\n        </div>\n    </div>\n</div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/cards.html",
    "content": "{% macro pair_status_card(pair, status, watched_symbols, displayed_portfolio, symbols_values, ref_market) -%}\n    {% set symbol = pair.split('/')[0] %}\n    <!-- Card -->\n    <div class=\"card card-status-color pair_status_card mb-4 small-size\">\n\n    <!--Title-->\n    <div class=\"card-header\">{{ pair }}\n        <a class=\"float-right\">\n            <i class=\"{{'fas' if pair in watched_symbols else 'far'}} fa-star ml-auto watched_element\" symbol=\"{{ pair }}\" update_url=\"{{ url_for('watched_symbols') }}\"\n               data-toggle=\"tooltip\" data-placement=\"top\" title=\"Add to / Remove from watched markets\"></i>\n        </a>\n    </div>\n\n    <!--Card image-->\n    <div class=\"view overlay animated fadeIn text-center pt-2\">\n      <img class=\"img-fluid mx-auto currency-image small-image\"\n           src=\"{{ url_for('static', filename='img/svg/loading_currency.svg') }}\"\n           alt=\"{{ symbol }} :(\"\n           data-symbol=\"{{symbol.lower()}}\">\n        <a href=\"{{ url_for('symbol_market_status', exchange_id=status.keys()|list|first if status else '', symbol=pair) }}\">\n      </a>\n    </div>\n    <div class=\"card-body pb-3 px-3\">\n        <!--Card content-->\n        <div class=\"row text-center\">\n            <div class=\"col-6\">\n                {{ symbol }}\n            </div>\n            <div class=\"col-6\">\n                {{ ref_market }} equiv.\n            </div>\n            <div class=\"col-6 rounded-number\">\n                {{ displayed_portfolio[symbol][\"total\"] if symbol in displayed_portfolio else 0 }}\n            </div>\n            <div class=\"col-6 rounded-number\">\n                {{ symbols_values[symbol] if symbol in symbols_values else 0 }}\n            </div>\n        </div>\n        <div class=\"list-group list-group-flush\">\n            {% for exchange_id, evaluation in status.items() %}\n                <a class=\"btn btn-outline-primary status hover_anim px-3\"\n                   status=\"{{evaluation[0]}}\"\n                   href=\"{{ url_for('symbol_market_status', exchange_id=exchange_id, symbol=pair) }}\">\n                        {{ evaluation[2] }}\n                        {% if evaluation[1] %}\n                            : {{ evaluation[0] }}\n                            ({{ evaluation[1] }})\n                        {% endif %}\n                        <i class=\"float-right fa-solid fa-chart-column pt-1\"> </i>\n                </a>\n            {% endfor %}\n        </div>\n    </div>\n\n    </div>\n    <!-- Card -->\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/critical_notifications_alert.html",
    "content": "{% macro critical_notifications_alert(critical_notifications, maxDisplayed=5) %}\n    {% for notification in critical_notifications[0:maxDisplayed] %}\n        <div class=\"alert alert-danger\" role=\"alert\">\n            <h4 class=\"alert-title\">{{ notification[\"Title\"] }}</h4>\n            <div>{{ notification[\"Message\"] }}</div>\n        </div>\n    {% endfor %}\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/flash_messages.html",
    "content": "{% macro flash_messages() %}\n    {% with messages = get_flashed_messages(with_categories=true) %}\n        {% if messages %}\n            {% for category, message in messages %}\n                <div class=\"alert alert-{{ category }} alert-dismissible fade show\" role=\"alert\">\n                    {{ message }}\n                      <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                        <span aria-hidden=\"true\">&times;</span>\n                      </button>\n                </div>\n            {% endfor %}\n        {% endif %}\n    {% endwith %}\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/forms.html",
    "content": "{% macro render_field(field) %}\n    {{ field(**kwargs)|safe }}\n    {% if field.errors %}\n        <div class=errors>\n        {% for error in field.errors %}\n            <div>{{ error }}</div>\n        {% endfor %}\n        </div>\n    {% endif %}\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/major_issue_alert.html",
    "content": "{% macro major_issue_alert(major_issue_alerts) %}\n    {% for alert in major_issue_alerts %}\n        <div class=\"alert alert-danger\" role=\"alert\">\n            {{ alert }}\n        </div>\n    {% endfor %}\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/starting_waiter.html",
    "content": "{% macro display_loading_message(text=None, details=None, next_url=None) -%}\n    <div class=\"card w-75 mx-auto\">\n        <div class=\"card-header\"><h2>\n            {{ text or \"OctoBot is starting, please refresh this page in a few seconds.\" }}\n        </h2></div>\n        <div class=\"card-body text-center\" name=\"loadingSpinner\">\n            <div>\n                <i class=\"fa fa-spinner fa-spin fa-2xl\"></i>\n            </div>\n            {% if details %}\n            <div class=\"my-4\">\n                <h4>{{ details }}</h4>\n            </div>\n            {% endif %}\n        </div>\n        <div class=\"card-footer text-right\">\n        {% if next_url %}\n            <a type=\"button\" href=\"{{next_url}}\" class=\"btn btn-sm btn-outline-primary waves-effect\">Proceed anyway</a>\n        {% else %}\n            <button type=\"button\" id=\"reload-button\" class=\"btn btn-lg btn-primary waves-effect\">Refresh</button>\n        {% endif %}\n        </div>\n    </div>\n{%- endmacro %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/tables.html",
    "content": "{% macro order_tr(order, type='', timestamp='', sim_or_real='Simulated') -%}\n    <tr>\n        <td>{{ order.symbol }}</td>\n        <td class=\"text-center\">{{ type.replace(\"_\", \" \")}}</td>\n        <td>{{ order.origin_price if not order.origin_stop_price else order.origin_stop_price}}</td>\n        <td>{{ order.origin_quantity }}</td>\n        <td>{{ order.exchange_manager.exchange.name if order.exchange_manager else '' }}</td>\n        <td>{{ timestamp }}</td>\n        <td class=\"text-right\" data-order=\"{{ order.total_cost }}\">\n            {{ order.total_cost }} {{ order.market }}\n        </td>\n        <td scope=\"row\">\n            {% if sim_or_real == 'Simulated' %}\n                {{ sim_or_real }}\n            {% else %}\n                {{ sim_or_real }} {{\"(virtual)\" if order.is_self_managed()}}\n            {% endif %}\n        </td>\n        <td class=\"text-center py-1\">\n            <button type=\"button\" class=\"btn btn-sm btn-outline-danger waves-effect\" action=\"cancel_order\" order_desc=\"{{ order.order_id }}\" update-url=\"{{ url_for('api.orders', action='cancel_order') }}\"><i class=\"fas fa-ban\"></i></button>\n        </td>\n    </tr>\n{%- endmacro %}\n\n{% macro position_tr(position, sim_or_real='Simulated') -%}\n    {% if not position.is_idle() %}\n    <tr>\n        <td>{{ position.side.value | upper }} {{ position.symbol_contract }}</td>\n        <td>{{ position.size | round(5) }}</td>\n        <td>{{ position.value | round(5) }} {{ position.currency if position.symbol_contract.is_inverse_contract() else position.market }}</td>\n        <td>{{ position.entry_price | round(5) }}</td>\n        <td>{{ position.liquidation_price | round(5) }}</td>\n        <td>{{ position.margin | round(5) }} {{ position.currency if position.symbol_contract.is_inverse_contract() else position.market }}</td>\n        <td>{{ position.unrealized_pnl | round(5) }} {{ position.currency if position.symbol_contract.is_inverse_contract() else position.market }} ({{ position.get_unrealized_pnl_percent() | round(5) }}%)</td>\n        <td>{{ position.exchange_manager.exchange.name if position.exchange_manager else '' }}</td>\n        <td>{{ sim_or_real }}</td>\n        <td class=\"text-center py-1\">\n            <button type=\"button\" class=\"btn btn-sm btn-outline-danger waves-effect\" data-action=\"close_position\"\n                    data-position_symbol=\"{{ position.symbol }}\" data-position_side=\"{{ position.side.value }}\"\n                    data-update-url=\"{{ url_for('api.positions', action='close_position') }}\">\n                <i class=\"fas fa-ban\"></i>\n            </button>\n        </td>\n    </tr>\n    {% endif %}\n{%- endmacro %}\n\n{% macro trades_tr(trade, type='', timestamp='', sim_or_real='Simulated') -%}\n    <tr>\n        <td>{{ trade.symbol }}</td>\n        <td class=\"text-center\">{{ type.replace(\"_\", \" \")}}</td>\n        <td>{{ trade.executed_price }}</td>\n        <td>{{ trade.executed_quantity }}</td>\n        <td>{{ trade.exchange_manager.exchange.name if trade.exchange_manager else ''}}</td>\n        <td class=\"text-right\" data-order=\"{{ trade.total_cost }}\">{{ trade.total_cost }} {{ trade.market }}</td>\n        <td class=\"text-right\" data-order=\"{{ trade.fee['cost'] }}\">{{ trade.fee['cost'] }} {{ trade.fee['currency'] }}</td>\n        <td class=\"text-right\">{{ timestamp }}</td>\n        <td class=\"text-right\">{{ trade.trade_id }}</td>\n        <td>{{ sim_or_real }}</td>\n    </tr>\n{%- endmacro %}\n\n{% macro logs_tr(log) -%}\n    <tr\n    {% if \"ERROR\" in log[\"Level\"] %}\n        class=\"bg-danger\"\n    {% endif %}\n    {% if \"WARNING\" in log[\"Level\"] %}\n        class=\"bg-warning-dark\"\n    {% endif %}\n    >\n        <td>{{ log[\"Time\"] }}</td>\n        <td>{{ log[\"Level\"] }}</td>\n        <td>{{ log[\"Source\"] }}</td>\n        <td>{{ log[\"Message\"] }}</td>\n    </tr>\n{%- endmacro %}\n\n{% macro notifications_tr(notification) -%}\n    <tr\n    {% if \"ERROR\" in notification[\"Level\"] %}\n        class=\"bg-danger\"\n    {% endif %}\n    {% if \"WARNING\" in notification[\"Level\"] %}\n        class=\"bg-warning-dark\"\n    {% endif %}\n    >\n        <td>{{ notification[\"Time\"] }}</td>\n        <td>{{ notification[\"Title\"] }}</td>\n        <td>{{ notification[\"Message\"] }}</td>\n        <td>{{ notification[\"Level\"] }}</td>\n    </tr>\n{%- endmacro %}\n\n{% macro top_tr(item) -%}\n    <tr\n    {% if item[\"rank\"] == 0 %}\n        class=\"unique-color-dark\"\n    {% elif item[\"rank\"] == 1 %}\n        class=\"unique-color\"\n    {% elif item[\"rank\"] == 2 %}\n        class=\"special-color\"\n    {% endif %}\n    >\n        <td scope=\"row\">{{ item[\"rank\"] }}</td>\n        <td class=\"text-capitalize\">{{ item[\"name\"] }}</td>\n        <td>{{ item[\"count\"] }}</td>\n    </tr>\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/tentacles.html",
    "content": "{% import 'macros/text.html' as m_text %}\n\n{% macro tentacle_description(info, strategy, name, read_only=False) %}\n      <span class=\"markdown-content\">\n            {{info['description']}}\n      </span>\n      {% if not read_only and info['default-config'] %}\n            <p>\n                {{ default_config_with_apply(info['default-config'], strategy, name, info['requirements']) }}\n            </p>\n      {% endif %}\n{% endmacro %}\n\n{% macro tentacle_horizontal_description(info, strategy) %}\n    <div class=\"row\">\n    {{ tentacle_horizontal_description_row_content(info, strategy, not info['requirements']) }}\n    {% if info['requirements'] %}\n        <div class=\"col border-left border-dark\">\n            {{ requirements_and_default_config(info['requirements'], info['default-config'], strategy, info['name']) }}\n        </div>\n    {% endif %}\n    </div>\n{% endmacro %}\n\n{% macro tentacle_horizontal_description_row_content(info, strategy, use_all_cols) %}\n    {% if info['description'] %}\n    <div class=\"col{{'' if use_all_cols else '-md-8'}} markdown-content\">\n        {{info['description']}}\n    </div>\n    {% endif %}\n{% endmacro %}\n\n{% macro tentacle_with_link(tentacle_name) %}\n    <a href=\"{{ url_for('config_tentacle', name=(tentacle_name)) }}\">{{ tentacle_name }}</a>\n{% endmacro %}\n\n{% macro tentacle_with_link_list(tentacle_list) %}\n    {% if tentacle_list == [\"*\"] %}\n        All tentacles returning values between -1 and 1.\n    {% else %}\n        {% for tentacle in tentacle_list %}\n            {{ tentacle_with_link(tentacle) }}\n        {% endfor %}\n    {% endif %}\n{% endmacro %}\n\n{% macro default_config_with_apply(default_config, strategy, name, requirements) %}\n    <div>\n        <h5>Default configuration:\n            {% if strategy or (not strategy and requirements|length > 1) %}\n             <button type=\"button\"\n                   config-type=\"evaluator_list_config\"\n                   id=\"applyDefaultConfig\"\n                   tentacle=\"{{ name }}\"\n                   class=\"btn btn-sm btn-outline-green waves-effect config-element float-right\">\n               <i class=\"fas fa-check\"></i> Activate\n            </button>\n            {% endif %}\n        </h5>\n    </div>\n    {{ tentacle_with_link_list(default_config) }}\n{% endmacro %}\n\n{% macro requirements_and_default_config(requirements, default_config, strategy, name) %}\n    <h5>Compatible {{ 'evaluators' if strategy else 'strategies' }}:</h5>\n        {{ tentacle_with_link_list(requirements) }}\n    <hr>\n    {{ default_config_with_apply(default_config, strategy, name, requirements) }}\n{% endmacro %}\n\n{% macro missing_tentacles_warning(missing_tentacles) %}\n    {% if missing_tentacles %}\n        <div class=\"alert alert-warning\" role=\"alert\">\n            <h4>Missing tentacles</h4>\n            <p>\n                <span class=\"font-weight-bold\">{{ missing_tentacles | join(', ') }}</span>\n                tentacle{{\"s are\" if missing_tentacles|length > 1 else \" is\"}} missing in\n                your OctoBot tentacles but {{\"are\" if missing_tentacles|length > 1 else \" is\"}} referenced in your\n                current profile. Your OctoBot might not work as expected if those are necessary.\n                </p>\n        </div>\n    {% endif %}\n{% endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/text.html",
    "content": "{% macro text_lines(lines) -%}\n    {% for line in lines %}\n        <p>\n            {{ line }}\n        </p>\n    {% endfor %}\n{%- endmacro %}\n\n{% macro text_split_lines(text) -%}\n    {% for line in text.split(\"\\n\") %}\n        {% if not loop.first %}\n            <br>\n        {% endif %}\n        {{ line }}\n    {% endfor %}\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/macros/trading_state.html",
    "content": "{% macro display_trading_state(is_real_trading, enabled_trader, hide_on_small=True, use_bold_for_title=False) -%}\n    <span class=\"{% if hide_on_small %}d-none d-xxl-inline{% endif %}{% if use_bold_for_title %} font-weight-bold{% endif %}\">{{ enabled_trader }} </span>\n    <i class=\"fa {% if is_real_trading %}fa-coins{% else %}fa-robot{% endif %}\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"{{ enabled_trader }}\"></i>\n{%- endmacro %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/octobot_help.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"help\" %}\n\n{% block body %}\n<br>\n<div class=\"card w-md-75 mx-auto\">\n    <div class=\"card-header\">\n        <h2>Understanding OctoBot</h2>\n    </div>\n    <div class=\"card-body pt-1\">\n        <p class=\"help-section\">\n            <h4>\n                Tutorials\n            </h4>\n            <a class=\"btn btn-sm btn-primary waves-effect\" href=\"{{url_for('home', reset_tutorials=True)}}\">Reset introduction tutorials</a>\n        </p>\n        <p class=\"help-section\">\n            <h4>\n                Help buttons: <a class=\"blue-text\" target=\"_blank\"><i class=\"fa-solid fa-question\"></i></a>\n            </h4>\n            When using OctoBot, you will find these buttons: <a class=\"blue-text\" target=\"_blank\"><i class=\"fa-solid fa-question\"></i></a>.\n            They are triggering the in page help and contain links to the\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_WEBSITE_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_help_buttons\"> OctoBot website</a> or\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_help_buttons\"> OctoBot guides</a> explaining\n            to the associated element.\n        </p>\n        <p class=\"help-section\">\n            <h4>\n                Frequently asked questions\n            </h4>\n            We keep track of many of our community users questions so that everyone can benefit from the answers in\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-usage/frequently-asked-questions-faq?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_help_faq\">our dedicated FAQ</a>.\n        </p>\n        <p class=\"help-section\">\n            <h4>\n                Troubleshoot\n            </h4>\n            Some issues are pretty common and sometimes they are due to factors that are external to OctoBot. In the\n            <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot-installation/troubleshoot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_help_troubleshoot\">troubleshoot section </a> you will\n            find many possible issues happening on various situations and how to fix them.\n        </p>\n        <p class=\"help-section\">\n            <h4>\n                OctoBot cloud\n            </h4>\n            In the <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_WEBSITE_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_help_details\"> OctoBot website</a>, you will find many resources on various subjects including:\n            <ul>\n                <li>\n                    What is the OctoBot Project\n                </li>\n                <li>\n                    In depth insight regarding OctoBot, its design and philosophy\n                </li>\n            </ul>\n            <h4>\n                OctoBot guides\n            </h4>\n            In the <a target=\"_blank\" rel=\"noopener\" href=\"{{OCTOBOT_DOCS_URL}}/octobot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=octobot_help_details\"> OctoBot guides</a>, you will find many articles to help you use OctoBot including:\n            <ul>\n                <li>\n                    Video guides on OctoBot's setup and main features\n                </li>\n                <li>\n                    Different ways to install OctoBot on your own computer or on the cloud\n                </li>\n                <li>\n                    OctoBot configuration\n                </li>\n                <li>\n                    OctoBot strategies and trading modes\n                </li>\n                <li>\n                    Supported exchanges\n                </li>\n                <li>\n                    Advanced resources on OctoBot architecture, development guides and specific features\n                </li>\n                <li>\n                    OctoBot Script\n                </li>\n            </ul>\n        </p>\n        <p class=\"help-section\">\n            <h4>\n                Telegram and Discord channels\n            </h4>\n            In case none of the above resources helped you to solve your issue or answer your question, you can always ask for help in the\n            <a class href=\"https://t.me/joinchat/F9cyfxV97ZOaXQ47H5dRWw\" target=\"_blank\" rel=\"noopener\"><i class=\"fab fa-telegram\"></i> Telegram</a>\n            or <a class href=\"https://discordapp.com/invite/vHkcb8W\" target=\"_blank\" rel=\"noopener\"><i class=\"fab fa-discord\"></i> Discord</a>\n            community channels.\n        </p>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/portfolio.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"portfolio\" %}\n{% import 'macros/cards.html' as m_cards %}\n{% import 'macros/starting_waiter.html' as m_waiter %}\n{% block body %}\n\n<div id=\"portfolio-display\">\n    {% macro display_init_warning() -%}\n        {% if initializing_currencies_prices %}\n        <div class=\"alert alert-warning\" role=\"alert\">\n            OctoBot is currently initializing prices for {{ initializing_currencies_prices | join(\", \") }}.\n            These assets might take a few seconds to load.\n        </div>\n        {% endif %}\n    {%- endmacro %}\n\n    {% macro holding_row(holdings, holding_type) -%}\n            <td class=\"align-middle rounded-number\" data-toggle=\"tooltip\" title=\"{{get_exchange_holdings(holdings, holding_type)}}\">\n                {{holdings[holding_type]}}\n            </td>\n    {%- endmacro %}\n\n    {% macro portfolio_holding(currency, holdings, value) -%}\n        <tr class=\"symbol-holding text-center\">\n            <td class=\"row mx-0\">\n                <div class=\"col col-md-5 animated px-2 fadeIn img-fluid very-small-size\">\n                    <img class=\"card-img-top currency-image\"\n                         src=\"{{ url_for('static', filename='img/svg/loading_currency.svg') }}\"\n                         alt=\"{{currency}}\"\n                         data-symbol=\"{{currency.lower()}}\">\n                </div>\n                <div class=\"d-none d-md-inline col-7 my-auto\">\n                    <span class=\"symbol\">{{currency}}</span></div>\n            </td>\n            {{ holding_row(holdings, \"total\") }}\n            <td class=\"total-value align-middle rounded-number\">{{value}}</td>\n            {{ holding_row(holdings, \"free\") }}\n            {{ holding_row(holdings, \"locked\") }}\n        </tr>\n    {%- endmacro %}\n\n    <br>\n    {% if not has_real_trader and not has_simulated_trader %}\n        {{ m_waiter.display_loading_message(details=\"If this message remains, please make sure that at least one exchange is enabled in your profile.\") }}\n    {% else %}\n        <div class=\"card\" id=\"portfoliosCard\" reference_market=\"{{reference_unit}}\">\n            {{ display_init_warning() }}\n            <div class=\"card-header\"><h2>Portfolio: <span class=\"rounded-number\">{{displayed_portfolio_value}}</span> {{reference_unit}}</h2></div>\n            <div class=\"card-body row mx-0 justify-content-center\">\n                {% if displayed_portfolio %}\n                    <div class=\"col-12 col-md-6 mb-2 mb-md-4\" id=\"portfolio_doughnutChart\"\n                         data-md-height=\"350\" data-sm-height=\"200\">\n                    </div>\n                    <div class=\"col-12\">\n                        <table class=\"table table-striped table-responsive-sm\" id=\"holdings-table\">\n                          <thead>\n                            <tr class=\"text-center\">\n                                <th scope=\"col\">Asset</th>\n                                <th scope=\"col\">Total</th>\n                                <th scope=\"col\">Value in {{reference_unit}}</th>\n                                <th scope=\"col\">Available</th>\n                                <th scope=\"col\">Locked in orders</th>\n                            </tr>\n                          </thead>\n                          <tbody>\n                            {% for currency, holdings in displayed_portfolio.items() %}\n                                {{ portfolio_holding(currency, holdings, symbols_values[currency]) }}\n                            {% endfor %}\n                          </tbody>\n                        </table>\n                    </div>\n                {% else %}\n                    <div class=\"card-subtitle\">\n                        <h2 class=\"text-muted\">Nothing there.</h2>\n                        <p>\n                            If a trader is enabled, please check <a href=\"{{url_for('logs')}}\">your OctoBot logs</a>.\n                            There might be an issue with your exchange credentials.\n                        </p>\n                    </div>\n                {% endif %}\n            </div>\n            <div class=\"card-footer d-flex justify-content-end\">\n                <div class=\"d-flex justify-content-end\">\n                    <button\n                        data-url=\"{{ url_for('api.clear_portfolio_history') }}\"\n                        id=\"clear-portfolio-history-button\"\n                        class=\"btn btn-outline-warning waves-effect\"\n                        data-toggle=\"tooltip\"\n                        data-placement=\"top\"\n                        title=\"Reset portfolio and profitability historical values.\"\n                    >\n                        <i class=\"fas fa-trash\"></i> Reset history\n                    </button>\n                    {% if has_real_trader%}\n                    <button id=\"refresh-portfolio\" update-url=\"{{ url_for('api.refresh_portfolio') }}\"\n                            class=\"btn btn-outline-danger btn-lg waves-effect\"\n                            data-toggle=\"tooltip\"\n                            data-placement=\"top\"\n                            title=\"Triggers a total portfolios re-synchronization using exchanges as a reference.\"\n                    >\n                        <i class=\"fa fa-sync\"></i> Force refresh\n                    </button>\n                    {% endif %}\n                </div>\n            </div>\n        </div>\n    {% endif %}\n    <br>\n    {% endblock %}\n</div>\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/portfolio.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/profile.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n{% set startup_messages_added_classes = \"justify-content-end px-4\" %}\n{% set inner_startup_messages_added_classes = \"offset-md-3 offset-lg-2 offset-1\" %}\n{% import 'components/config/exchange_card.html' as m_config_exchange_card %}\n{% import 'components/config/currency_card.html' as m_config_currency_card %}\n{% import 'components/config/trader_card.html' as m_config_trader_card %}\n{% import 'components/config/evaluator_card.html' as m_config_evaluator_card %}\n{% import 'components/config/tentacle_card.html' as m_config_tentacle_card %}\n{% import 'components/config/profiles.html' as m_config_profile_tab %}\n{% import 'macros/tentacles.html' as m_tentacles %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n\n{% set config_default_value = \"Bitcoin\" %}\n{% set config_default_symbol = \"btc\" %}\n{% set added_class = \"new_element\" %}\n\n\n{% block additional_style %}\n<link rel=\"stylesheet\"\n      href=\"{{ url_for('static', filename='css/components/configuration.css', u=LAST_UPDATED_STATIC_FILES) }}\">\n{% endblock additional_style %}\n\n{% block body %}\n\n<div class=\"row mt-md-4 mt-2\">\n    <nav class=\"mt-md-4 mt-2 col-md-3 col-lg-2 col-1 d-block sidebar shadow\">\n        <div class=\"sidebar-sticky mt-0 pt-0\">\n            <div class=\"nav flex-column pt-0 mt-0 mt-md-4\" id=\"v-tab\" role=\"tablist\"\n                 aria-orientation=\"vertical\">\n                <span>\n                    <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex justify-content-between dropdown-toggle collapsed\"\n                       data-toggle=\"collapse\"\n                       id=\"edit-profiles-button\"\n                       href=\"#profilesSubmenu\" role=\"tab\" aria-expanded=\"false\">\n                        <span class=\"d-flex\">\n                            <i class=\"fas fa-users\"></i><span class=\"d-none d-md-block pl-3\">Edit profiles</span>\n                        </span>\n                    </a>\n                    <ul class=\"collapse nav\" id=\"profilesSubmenu\">\n                        {% for profile_id, profile in profiles.items() | sort(attribute='1.name') %}\n                        <li class=\"w-100\">\n                            <a class=\"nav-link pl-2 pl-sm-4 waves-effect d-flex {{ 'font-weight-bold' if profile_id == current_profile.profile_id }}\" id=\"profile-{{profile_id}}-tab\"\n                               data-toggle=\"pill\"\n                               href=\"#panelProfile{{profile_id}}\" role=\"tab\" aria-controls=\"panelProfile{{profile_id}}\"\n                               aria-selected=\"false\">\n                                <i class=\"fas {{'fa-user-lock' if profile.read_only else 'fa-user '}}\"></i>\n                                <span class=\"d-none d-md-block pl-3\" data-role=\"profile-name\" data-profile-id=\"{{profile_id}}\">{{profile.name}}</span>\n                                {% if profile_id == current_profile.profile_id %}\n                                <i class=\"fas fa-check ml-1 ml-md-2\"></i>\n                                {% endif %}\n                            </a>\n                        </li>\n                        {% endfor %}\n                    </ul>\n                </span>\n                <span class=\"separator mb-2 mt-2 mt-md-4 pl-2 pl-sm-3\">\n                    <span class=\"d-none d-md-block text-muted\">\n                        Profile configuration\n                    </span>\n                </span>\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" data-tab=\"default\" id=\"panelStrategies-tab\" data-toggle=\"pill\"\n                   href=\"#panelStrategies\" role=\"tab\" aria-controls=\"panelStrategies\" aria-selected=\"true\">\n                    <i class=\"fab fa-octopus-deploy my-auto\"></i><span class=\"d-none d-md-block pl-3\">Strategies</span>\n                </a>\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelCurrency-tab\" data-toggle=\"pill\"\n                   href=\"#panelCurrency\" role=\"tab\" aria-controls=\"panelCurrency\" aria-selected=\"false\">\n                    <i class=\"fab fa-bitcoin my-auto\"></i><span class=\"d-none d-md-block pl-3\">Currencies</span>\n                </a>\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelExchanges-tab\" data-toggle=\"pill\"\n                   href=\"#panelExchanges\" role=\"tab\" aria-controls=\"panelExchanges\" aria-selected=\"false\">\n                    <i class=\"fas fa-exchange-alt my-auto\"></i><span class=\"d-none d-md-block pl-3\">Exchanges</span>\n                </a>\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelTrading-tab\" data-toggle=\"pill\"\n                   href=\"#panelTrading\" role=\"tab\" aria-controls=\"panelTrading\" aria-selected=\"false\">\n                    <i class=\"fas fa-wallet my-auto\"></i><span class=\"d-none d-md-block pl-3\">Trading</span>\n                </a>\n                {% if config_tentacles_by_group or other_tentacles_config %}\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelTentacles-tab\" data-toggle=\"pill\"\n                   href=\"#panelTentacles\" role=\"tab\" aria-controls=\"panelTentacles\" aria-selected=\"false\">\n                    <i class=\"fas fa-cogs my-auto\"></i><span class=\"d-none d-md-block pl-3\">Tentacles</span>\n                </a>\n                {% endif %}\n                {% if are_automations_enabled %}\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panelAutomations-tab\" data-toggle=\"pill\"\n                   href=\"#panelAutomations\" role=\"tab\" aria-controls=\"panelAutomations\" aria-selected=\"false\">\n                    <i class=\"fas fa-robot my-auto\"></i><span class=\"d-none d-md-block pl-3\">Automations</span>\n                </a>\n                {% endif %}\n            </div>\n            <button class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0 mx-md-auto\" id=\"save-config\" href=\"#\"\n               aria-selected=\"false\" update-url=\"{{ url_for('config') }}\">\n                <i class=\"fas fa-save my-auto\"></i><span class=\"d-none d-md-block pl-2\">Save</span>\n            </button>\n            <button class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0 mx-md-auto\" id=\"reset-config\" href=\"#\"\n               aria-selected=\"false\">\n                <i class=\"fas fa-redo-alt my-auto\"></i><span class=\"d-none d-md-block pl-2\">Reset all</span>\n            </button>\n            <button class=\"w-100 my-3 p-2 pl-sm-3 btn btn-lg btn-outline-primary waves-effect d-flex mx-0 mx-md-auto mt-3 mt-mb-5 mb-5\"\n                    id=\"save-config-and-restart\" href=\"#\" type=\"button\" aria-selected=\"false\"\n                    update-url=\"{{ url_for('config') }}\">\n                <i class=\"fas fa-power-off my-auto\"></i><span class=\"d-none d-md-block pl-2\">Apply changes and restart</span>\n            </button>\n        </div>\n    </nav>\n    <main role=\"main\" class=\"col-md-9 col-lg-10 col-11 ml-auto px-4\">\n        <div>\n            {{ m_flash_messages.flash_messages() }}\n        </div>\n        {% if not strategy_config[\"trading-modes\"] %}\n        <div class=\"alert alert-danger\" role=\"alert\">\n            <h4>Configuration issue</h4>\n            <p>\n                An error occurred when loading your configuration.\n                Editing your OctoBot configuration might currently be impossible.\n                Please report this error to the OctoBot team if you see it.\n            </p>\n        </div>\n        {% endif %}\n        <div class=\"tab-content\" id=\"super-container\">\n            <div class=\"card card-header \">\n                <h2>\n                    <span class=\"d-none d-md-inline-block\">Current profile</span>\n                    <span class=\"font-weight-bold\" data-role=\"profile-name\" data-profile-id=\"{{current_profile.profile_id}}\">{{current_profile.name}}</span>\n                    <a class=\"d-inline-block\" data-profile-id=\"{{current_profile.profile_id}}\" data-role=\"current-profile-selector\">\n                        <button class=\"btn btn-sm btn-outline-primary align-middle\">\n                            View profile\n                        </button>\n                    </a>\n                    <a class=\"btn btn-sm rounded-circle btn-primary waves-effect mx-1 mx-md-4 align-middle\"\n                    href=\"{{url_for('profiles_selector')}}\" data-toggle=\"tooltip\"\n                    title=\"Select another profile\"\n                    id=\"profile-selector-link\"\n                    >\n                        <i class=\"fas fa-exchange-alt\" aria-hidden=\"true\"></i>\n                    </a>\n                </h2>\n            </div>\n            {% for profile_id, profile in profiles.items() %}\n            {{m_config_profile_tab.profile_tab(current_profile, profile, profiles_tentacles_details[profile_id], strategy_config, evaluator_config,\n                                               get_profile_traded_pairs_by_currency, get_profile_exchanges, get_enabled_trader, get_filtered_list, OCTOBOT_DOCS_URL)}}\n            {% endfor %}\n            <div class=\"tab-pane fade config-root show\" id=\"panelStrategies\" role=\"tabpanel\"\n                 aria-labelledby=\"panelStrategies-tab\">\n                {% if profiles_tentacles_details[current_profile.profile_id][\"read_error\"] %}\n                    <div class=\"alert alert-warning\" role=\"alert\">\n                        <h4>Configuration warning</h4>\n                        <p>\n                            An error occurred when loading this profile. More details in logs.\n                        </p>\n                    </div>\n                {% elif profiles_tentacles_details[current_profile.profile_id][\"version\"] != CURRENT_BOT_VERSION\n                    and not profiles_tentacles_details[current_profile.profile_id][\"imported\"]\n                    and profiles_tentacles_details[current_profile.profile_id][\"require_exact_version\"]%}\n                    <div class=\"alert alert-warning\" role=\"alert\">\n                        <h4>Profile version warning</h4>\n                        <p>\n                            Your current profile ({{ current_profile.name }}) has been imported from an OctoBot on version\n                            {{ profiles_tentacles_details[current_profile.profile_id][\"version\"] }}.\n                            Please make sure that this profile is compatible with OctoBot {{ CURRENT_BOT_VERSION }}\n                            or download a newer version.\n                        </p>\n                    </div>\n                {% endif %}\n\n                {% set trading_modes = strategy_config[\"trading-modes\"].items() %}\n                {% set strategies = strategy_config[\"strategies\"].items() %}\n\n                {% if not current_profile.read_only %}\n                    <div class=\"card\">\n                        <div class=\"card-header\">\n                            <h2>\n                                Trading modes\n                                <a class=\"float-right blue-text\" target=\"_blank\" data-intro=\"profile\" >\n                                    <i class=\"fa-solid fa-question\"></i>\n                                </a>\n                            </h2>\n                        </div>\n                        <div class=\"card-body\">\n                            <div class=\"row config-container\" id=\"trading-modes-config-root\">\n                                {% for trading_mode_name, info in trading_modes %}\n                                    {{ m_config_evaluator_card.config_evaluator_card(trading_startup_config, trading_mode_name,\n                                    info, \"trading_config\", include_modal=False) }}\n                                {% endfor %}\n                            </div>\n                        </div>\n                        <div class=\"card-footer\">\n                            <div class=\"quote px-2 font-weight-bold\" id=\"selected-trading-mode-summary\"></div>\n                        </div>\n                    </div>\n                    <br>\n\n                    <div class=\"card\">\n                        <div class=\"card-header\">\n                            <h2>\n                                Compatible evaluation strategies\n                            </h2>\n                        </div>\n                        <div class=\"card-body\">\n                            <div class=\"d-none row mx-3\" id=\"no-strategy-info\">\n                                <h4>This trading mode doesn't need any strategy.</h4>\n                            </div>\n                            <div class=\"d-none row config-container\" id=\"evaluator-config-root\">\n                                {% for evaluator_name, info in strategies %}\n                                    {{ m_config_evaluator_card.config_evaluator_card(evaluator_startup_config, evaluator_name,\n                                    info, \"evaluator_config\", strategy=True, include_modal=False) }}\n                                {% endfor %}\n                            </div>\n                        </div>\n                        <div class=\"card-footer\" id=\"evaluator-config-root-footer\">\n                            <div class=\"quote px-2 font-weight-bold\">\n                                Customize the time frames to trade on in your strategy configuration.\n                            </div>\n                            <p class=\"mt-3 mb-0\"><i class=\"fab fa-octopus-deploy\"></i> Pro tip: customize evaluators to use with your strategy the\n                                <a href=\"{{ url_for('advanced.evaluator_config') }}\">\n                                    advanced evaluators configuration\n                                </a>\n                            </p>\n                        </div>\n                    </div>\n                    <br>\n                    {{ m_tentacles.missing_tentacles_warning(missing_tentacles) }}\n                {% else %}\n                    <div class=\"card\">\n                        <div class=\"card-header\">\n                            <h2>\n                                Profile strategy configuration\n                                <span\n                                    href=\"\"\n                                    class=\"badge badge-info waves-effect align-top\"\n                                    data-role=\"current-profile-selector\"\n                                    data-toggle=\"tooltip\"\n                                    title=\"Duplicate this profile to select other trading modes or evaluators\"\n                                >read only</span>\n                                <a class=\"float-right blue-text\" data-intro=\"profile\">\n                                    <i class=\"fa-solid fa-question\"></i>\n                                </a>\n                            </h2>\n                        </div>\n                        <div class=\"card-body mt-0 pt-2 mb-0 pb-0\">\n                            {% for trading_mode_name, info in trading_modes %}\n                                {% if info['activation'] %}\n                                    <div class=\"card mb-4 border-primary\">\n                                        <div class=\"card-header\">\n                                            <h4>\n                                                {{ trading_mode_name }}\n                                                <a href=\"{{ url_for('config_tentacle', name=(trading_mode_name)) }}\"\n                                                   class=\"align-top\" role=\"button\"><i class=\"fa fa-cog\"></i></a>\n                                            </h4>\n                                        </div>\n\n                                        <div class=\"card-body pb-0\">\n                                            <div class=\"row\">\n                                                {{ m_tentacles.tentacle_horizontal_description_row_content(info, tentacle_type==\"strategy\", True) }}\n                                                <div class=\"col\">\n                                                    <div class=\"alert alert-info\" role=\"alert\">\n                                                        <h4 class=\"alert-heading\">Trading modes</h4>\n                                                        <p>\n                                                            Trading modes are responsible for creating, updating or cancelling orders.\n                                                            They can use strategy(ies) to receive signals and create orders accordingly.\n                                                        </p>\n                                                        <p>\n                                                            Lean more about OctoBot trading modes on\n                                                            <a target=\"_blank\" rel=\"noopener\"\n                                                               href=\"{{OCTOBOT_DOCS_URL}}/octobot-trading-modes/trading-modes?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=read_only_profile\">\n                                                                the trading modes guides\n                                                            </a>.\n                                                        </p>\n                                                    </div>\n                                                  </div>\n                                            </div>\n                                        </div>\n                                    </div>\n                                {% endif %}\n                            {% endfor %}\n\n                            {% for evaluator_name, info in strategies %}\n                                {% if info['activation'] %}\n                                    <div class=\"card mb-4 border-warning\">\n                                        <div class=\"card-header\">\n                                            <h4>\n                                                {{ evaluator_name }}\n                                                <a href=\"{{ url_for('config_tentacle', name=(evaluator_name)) }}\"\n                                                   class=\"align-top\" role=\"button\"><i class=\"fa fa-cog\"></i></a>\n                                            </h4>\n                                        </div>\n\n                                        <div class=\"card-body pb-0\">\n                                            <div class=\"row\">\n                                                {{ m_tentacles.tentacle_horizontal_description_row_content(info, tentacle_type==\"strategy\", True) }}\n                                                <div class=\"col\">\n                                                    <div class=\"alert alert-info\" role=\"alert\">\n                                                        <h4 class=\"alert-heading\">Strategies</h4>\n                                                        Strategies create input signals for trading modes using technical, social and real time analysis provided by configured evaluators.<br>\n                                                    </div>\n                                                </div>\n                                            </div>\n                                        </div>\n                                    </div>\n                                {% endif %}\n                            {% endfor %}\n                        </div>\n                        <div class=\"card-footer\">\n                        </div>\n                    </div>\n                {% endif %}\n                {% if not has_open_source_package() %}\n                <div class=\"text-center\">\n                    <i class=\"fa fa-info-circle\"></i> The <a href=\"{{ url_for('extensions') }}\">{{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}</a> contains many other configuration profiles and enables\n                    your open source OctoBot to use and customize OctoBot cloud's <a target=\"_blank\" rel=\"noopener\" href=\"https://app.octobot.cloud/explore?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=Profile\">automatically configured indexes</a>.\n                </div>\n                {% endif %}\n            </div>\n            <div class=\"tab-pane fade config-root\" id=\"panelCurrency\" role=\"tabpanel\"\n                 aria-labelledby=\"panelCurrency-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\"><h2>Currencies</h2></div>\n                    <div class=\"card-body deck-container\">\n                        {% if in_backtesting %}\n                        <div class=\"alert alert-warning\" role=\"alert\">\n                            OctoBot is currently running in backtesting analysis, information in this section may not be\n                            accurate and changes won't be saved.\n                        </div>\n                        {% endif %}\n                        <div class=\"card\">\n                            <div class=\"card-body\">\n                                <div class=\"container-fluid row col-12\">\n                                    <div class=\"col-md-6\">\n                                        <label for=\"AddCurrencySelect\">Add a cryptocurrency to trade:</label>\n                                        <select id=\"AddCurrencySelect\" data-live-search=\"true\" data-window-padding=\"25\"\n                                                reference_market=\"{{ config_reference_market }}\" data-fetch-url=\"{{ url_for('api.currency_list') }}\">\n\n                                        </select>\n                                        <button type=\"button\" id=\"AddCurrency\"\n                                                class=\"btn btn-primary add-btn px-3 waves-effect\"><i class=\"fa fa-plus pr-2\"\n                                                                                                     aria-hidden=\"true\"></i> Add\n                                        </button>\n                                    </div>\n                                    <div class=\"d-none d-md-inline-block col-md-6\">\n                                        <div class=\"d-flex justify-content-end\">\n                                            <button class=\"btn btn-outline-info btn-lg mx-1 waves-effect\" id=\"export-currencies-button\"\n                                                    update-url=\"{{ url_for('api.get_config_currency')}}\">\n                                                <i class=\"fa fa-cloud-download-alt\"></i> <span class=\"d-none d-md-inline-block\">Export</span>\n                                            </button>\n                                            <input class=\"d-none\" id=\"import-currencies-input\" type=\"file\" name=\"file\" accept=\".json\"/>\n                                            <button class=\"btn btn-outline-info btn-lg mx-1 waves-effect\" id=\"import-currencies-button\"\n                                                    update-url=\"{{ url_for('api.set_config_currency') }}\">\n                                                <i class=\"fa fa-cloud-upload-alt\"></i> <span class=\"d-none d-md-inline-block\">Import</span>\n                                            </button>\n                                        </div>\n\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                        <br>\n                        {% if not symbol_list_by_type %}\n                        <div class=\"alert alert-danger\" role=\"alert\">Can't find any symbol suggestion because no\n                            exchange were configured.\n                        </div>\n                        <br>\n                        {% endif %}\n                        {% if \"future\" in enabled_exchange_types %}\n                        <div class=\"alert alert-info\" role=\"alert\">\n                             <i class=\"fa-regular fa-lightbulb\"></i>\n                            Only futures trading pairs can be traded on futures trading exchanges.\n                            Select pairs for the cryptocurrencies you wish to trade futures on using the trading pairs\n                            drop-down menu.\n                        </div>\n                        {% endif %}\n                        <!-- Card deck -->\n                        <div class=\"card-deck config-container\" update-url=\"{{ url_for('config') }}\">\n                            {% for crypto_currency in config_symbols %}\n                            {{ m_config_currency_card.config_currency_card(config_symbols, crypto_currency,\n                            filter_currency_pairs(crypto_currency, symbol_list_by_type, full_symbol_list, config_symbols),\n                            full_symbol_list, get_currency_id,\n                            symbol=config_symbols[crypto_currency]['pairs'][0].split('/')[0].lower() if\n                            config_symbols[crypto_currency]['pairs'])}}\n                            {% endfor %}\n                        </div>\n                    </div>\n                    <div class=\"card-footer\">\n                        <i class=\"fab fa-octopus-deploy\"></i> Pro tip: a pair you want to trade is not in the\n                        selector and your exchange is supporting it ? Just type it in and press enter.\n                        Data provided by <a href=\"https://www.coingecko.com/\" target=\"_blank\">CoinGecko</a>.\n                    </div>\n                </div>\n            </div>\n            <div class=\"tab-pane fade config-root\" id=\"panelExchanges\" role=\"tabpanel\"\n                 aria-labelledby=\"panelExchanges-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\">\n                        <h2>Exchanges\n                            <a class=\"float-right blue-text\" target=\"_blank\" rel=\"noopener\"\n                               href=\"{{EXCHANGES_DOCS_URL}}\">\n                                <i class=\"fa-solid fa-question\"></i>\n                            </a>\n                        </h2>\n                    </div>\n                    <div class=\"card-body deck-container\">\n                        {% if \"future\" in enabled_exchange_types %}\n                        <div class=\"alert alert-info\" role=\"alert\">\n                             <i class=\"fa-regular fa-lightbulb\"></i>\n                            Select leverage to use in futures trading from your <a href=\"{{url_for('config_tentacle', name=get_enabled_tentacles(trading_modes))}}\">trading mode configuration</a>.\n                        </div>\n                        {% endif %}\n                        <!-- Card deck -->\n                        <div class=\"card-deck config-container\" id=\"exchange-container\" update-url=\"{{ url_for('api.are_compatible_accounts') }}\">\n                            {% for exchange in config_exchanges %}\n                            {{ m_config_exchange_card.config_exchange_card(config_exchanges,\n                            exchange,\n                            exchanges_details[exchange],\n                            is_supporting_future_trading,\n                            enabled=config_exchanges[exchange].get('enabled', True),\n                            sandboxed=config_exchanges[exchange].get('sandboxed', False),\n                            selected_exchange_type=config_exchanges[exchange].get('exchange-type', exchanges_details[exchange]['default_exchange_type']),\n                            full_config=False) }}\n                            {% endfor %}\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"tab-pane fade config-root\" id=\"panelTrading\" role=\"tabpanel\" aria-labelledby=\"panelTrading-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\">\n                        <h2>\n                            Trading\n                            <span class=\"float-right badge {{ 'badge-info' if real_trader_activated else 'badge-primary' }} waves-effect\">\n                        {{ 'Real trader' if real_trader_activated else 'Simulator' }}\n                    </span>\n                        </h2>\n                    </div>\n                    <div class=\"card-body deck-container\">\n                        <!-- Card deck -->\n                        <div class=\"card-deck config-container\">\n                            {{ m_config_trader_card.config_trader_card(config_trading, \"trading\", \"Trading settings\",\n                            link=OCTOBOT_DOCS_URL+\"/octobot-configuration/profile-configuration#trading\") }}\n                            {{ m_config_trader_card.config_trader_card(config_trader, \"trader\", \"Trader\",\n                            link=OCTOBOT_DOCS_URL+\"/octobot-usage/simulator#real-trader\") }}\n                            {{ m_config_trader_card.config_trader_card(config_trader_simulator, \"trader-simulator\",\n                            \"Trader simulator\", link=OCTOBOT_DOCS_URL+\"/octobot-usage/simulator?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=simulator_config\",\n                            footer_text=\"Changes in the simulated starting portfolio will reset enabled exchanges simulated portfolio history.\") }}\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {% if config_tentacles_by_group or other_tentacles_config %}\n            <div class=\"tab-pane fade config-root\" id=\"panelTentacles\" role=\"tabpanel\"\n                 aria-labelledby=\"panelTentacles-tab\">\n                <div class=\"card\">\n                    <div class=\"card-body\">\n                        <div class=\"row config-container\">\n                            {% for config_tentacle in other_tentacles_config %}\n                            {{ m_config_tentacle_card.config_tentacle_card(config_tentacle[\"name\"], config_tentacle, False) }}\n                            {% endfor %}\n                        </div>\n                    </div>\n                </div>\n                <br>\n                {% for group, config_tentacles in config_tentacles_by_group.items() %}\n                <div class=\"card\">\n                    <div class=\"card-header text-capitalize\">\n                        <h2>{{group.replace(\"_\", \" \")}}</h2>\n                    </div>\n                    <div class=\"card-body\">\n                        <div class=\"row config-container\" id=\"{{group}}-config-root\">\n                            {% for config_tentacle in config_tentacles %}\n                            {{ m_config_tentacle_card.config_tentacle_card(config_tentacle[\"name\"], config_tentacle, True) }}\n                            {% endfor %}\n                        </div>\n                    </div>\n                </div>\n                <br>\n                {% endfor %}\n            </div>\n            {% endif %}\n            <div class=\"tab-pane fade config-root\" id=\"panelAutomations\" role=\"tabpanel\"\n                 aria-labelledby=\"panelTentacles-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\">\n                        <h4>\n                            Automations\n                        </h4>\n\n                    </div>\n                    <div class=\"card-body text-center\">\n                        <div>\n                            Automations are actions that will be triggered automatically when something happens. You can have as many automations as you want.\n                        </div>\n                        <div class=\"text-center my-5\">\n                            <div>\n                                <a type=\"button\" class=\"btn btn-primary btn-lg\"\n                                   href=\"{{ url_for('automations') }}\">\n                                    Configure your profile automations\n                                </a>\n                            </div>\n                            <div>\n                                Your current profile has <strong>{{automations_count}}</strong> automation{{'s' if automations_count > 1 else ''}}.\n                            </div>\n\n                        </div>\n                    </div>\n                </div>\n                <br>\n            </div>\n        </div>\n    </main>\n</div>\n<span class=\"d-none\" data-display-intro=\"{{display_intro}}\"></span>\n\n<!-- Modals -->\n{% for trading_mode_name, info in strategy_config[\"trading-modes\"].items() %}\n    {{ m_config_evaluator_card.evaluator_card_modal(trading_mode_name, info, False) }}\n{% endfor %}\n{% for evaluator_name, info in strategy_config[\"strategies\"].items() %}\n    {{ m_config_evaluator_card.evaluator_card_modal(evaluator_name, info, True) }}\n{% endfor %}\n{% for evaluator_type_items in ['ta', 'social', 'real-time'] %}\n    {% for evaluator_name, info in evaluator_config[evaluator_type_items].items() %}\n        {{ m_config_evaluator_card.evaluator_card_modal(evaluator_name, info, True) }}\n    {% endfor %}\n{% endfor %}\n{{ m_config_profile_tab.profile_import_modal() }}\n<!-- Default cards -->\n<div class=\"d-none\">\n    <!-- Currencies -->\n    <div id=\"AddCurrency-template-default\">\n        {{ m_config_currency_card.config_currency_card( config_symbols={config_default_value: {\"enabled\": true, \"pairs\":\n        [] } },\n        crypto_currency=config_default_value,\n        symbol_list_by_type=symbol_list_by_type,\n        full_symbol_list=full_symbol_list,\n        get_currency_id=get_currency_id,\n        add_class=added_class,\n        no_select=True,\n        additional_classes=\"default\",\n        symbol= config_default_symbol ) }}\n    </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/exchange_accounts.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/configuration.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/profile_management.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/profiles_selector.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n{% set startup_messages_added_classes = \"d-none\" %}\n\n{% import \"components/community/login.html\" as login %}\n{% import 'components/config/evaluator_card.html' as m_config_evaluator_card %}\n{% import 'components/community/cloud_strategies_selector.html' as m_cloud_strategies_selector %}\n{% import \"components/config/profiles.html\" as profiles_macros %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n{% import 'macros/starting_waiter.html' as m_starting_waiter %}\n\n{% block body %}\n<br>\n{% if not current_logged_in_email %}\n<div class=\"login_box mx-auto mb-2\">\n    <div class=\"card\">\n    {{ login.register_form(form, is_in_stating_community_env,\n                           url_for(\"profiles_selector\", onboarding=onboarding), after_login_action=\"sync_account\",\n                           details=\"Access your subscribed profiles as well as additional profiles.\") }}\n    </div>\n</div>\n{% endif %}\n<div class=\"card\">\n    <div class=\"card-header d-flex justify-content-between\">\n        <div>\n            <h3>\n                Select the profile your OctoBot should use\n            </h3>\n        </div>\n        <div class=\"text-right mt-4\">\n            <button class=\"btn btn-outline-primary waves-effect\"\n                    data-toggle=\"modal\" data-target=\"#importProfileModal\">\n                Import a profile\n            </button>\n        </div>\n    </div>\n    <div class=\"card-body pt-0\">\n        <div>\n            <div>\n                <ul class=\"nav nav-tabs md-tabs justify-content-center\" id=\"tabs\" role=\"tablist\">\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link primary-tab-selector {{ '' if use_cloud else 'active show' }}\" id=\"your-profiles-tab\" data-toggle=\"tab\" href=\"#your-profiles\" role=\"tab\"\n                           aria-controls=\"your-profiles\"\n                           aria-selected=\"{{ 'false' if use_cloud else 'true' }}\">\n                            <h5>Default</h5>\n                        </a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link primary-tab-selector {{ 'active show' if use_cloud else '' }}\" id=\"cloud-profiles-tab\" data-toggle=\"tab\" href=\"#cloud-profiles\" role=\"tab\"\n                           aria-controls=\"cloud-profiles\"\n                           aria-selected=\"{{ 'true' if use_cloud else 'false' }}\">\n                            <h5>From OctoBot cloud</h5>\n                        </a>\n                    </li>\n                </ul>\n            </div>\n            <div class=\"tab-content my-2\" id=\"tabcontent\">\n                <div class=\"tab-pane fade {{ '' if use_cloud else 'active show' }}\" id=\"your-profiles\" role=\"tab-your-profiles\" aria-labelledby=\"your-profiles-tab\">\n                    <h4 class=\"text-center mt-4\">\n                        Use ready-made profiles from the open-source OctoBot and your custom profiles.\n                    </h4>\n                    <div class=\"row mx-0 mt-1\">\n                        {% for profile in profiles %}\n                        {{ profiles_macros.profile_overview(profile, current_profile,\n                          profiles_tentacles_details[profile.profile_id], strategy_config,\n                          evaluator_config, get_profile_traded_pairs_by_currency, get_profile_exchanges,\n                          get_enabled_trader, get_filtered_list, read_only, True, onboarding) }}\n                        {% endfor %}\n                    </div>\n                </div>\n                <div class=\"tab-pane fade {{ 'active show' if use_cloud else '' }}\" id=\"cloud-profiles\" role=\"tab-cloud-profiles\" aria-labelledby=\"cloud-profiles-tab\">\n                    <div id=\"cloud-strategies-selector\"\n                         data-select-profile-url=\"{{url_for('profile', select='PROFILE_ID',\n                                                    next=url_for('trading_type_selector', reboot=True, onboarding=onboarding))}}\">\n                        <h4 class=\"text-center mt-4\">\n                            Use <a href=\"{{OCTOBOT_COMMUNITY_URL}}/strategies?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=profile_selector\" target=\"_blank\">OctoBot cloud strategies</a>\n                            directly from your OctoBot.\n                        </h4>\n                        {{ m_cloud_strategies_selector.cloud_strategies_selector(cloud_strategies, LOCALE, \"select-profile\") }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n\n<!--<span class=\"d-none\" data-display-intro=\"{{display_intro}}\"></span>-->\n\n<br>\n\n<!-- Modals -->\n{{ profiles_macros.profile_import_modal(url_for('profiles_selector')) }}\n{% for trading_mode_name, info in strategy_config[\"trading-modes\"].items() %}\n    {{ m_config_evaluator_card.evaluator_card_modal(trading_mode_name, info, False, read_only) }}\n{% endfor %}\n{% for evaluator_name, info in strategy_config[\"strategies\"].items() %}\n    {{ m_config_evaluator_card.evaluator_card_modal(evaluator_name, info, True, read_only) }}\n{% endfor %}\n{% for evaluator_type_items in ['ta', 'social', 'real-time'] %}\n    {% for evaluator_name, info in evaluator_config[evaluator_type_items].items() %}\n        {{ m_config_evaluator_card.evaluator_card_modal(evaluator_name, info, True, read_only) }}\n    {% endfor %}\n{% endfor %}\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/profile_management.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/community.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/profiles_selector.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/robots.txt",
    "content": "User-agent: *\nDisallow: /"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/symbol_market_status.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"trading\" %}\n{% block body %}\n<style>\n.card-deck .card{\n    max-width: 230px;\n}\n</style>\n<br>\n<div class=\"card\">\n    <div class=\"card-header d-flex\">\n        <h2>\n            <span class=\"float-left\">\n                <a href=\"{{ url_for('trading', _anchor='panel-market-status') }}\">\n                    <i class=\"fas fa-arrow-left\"></i>\n                </a>&nbsp\n            </span>\n        </h2>\n        <div class=\"animated fadeIn img-fluid very-small-size\">\n            <img class=\"card-img-top currency-image\"\n                 src=\"{{ url_for('static', filename='img/svg/loading_currency.svg') }}\"\n                 alt=\"{{currency}}\"\n                 data-symbol=\"{{symbol.split('/')[0].lower()}}\">\n        </div>\n        <div>\n            <h2>&nbsp{{symbol}} on {{exchange}}: <strong>{{symbol_evaluation}}</strong></h2>\n        </div>\n    </div>\n    <div class=\"card-body\">\n        <div class=\"text-center\">\n            Time frame\n            <select class=\"selectpicker\" data-live-search=\"true\" data-width=\"auto\" data-window-padding=\"25\" id=\"time-frame-select\">\n                {% for time_frame in time_frames %}\n                <option value={{time_frame.value}}>\n                {{time_frame.value}}\n                </option>\n                {% endfor %}\n            </select>\n        </div>\n        <br>\n        <div class=\"card-body candle-graph\" id=\"symbol_graph\" symbol=\"{{symbol}}\" exchange=\"{{exchange}}\" exchange_id=\"{{exchange_id}}\" backtesting_mode=\"{{ backtesting_mode }}\">\n            <div class=\"card-body text-center\" name=\"loadingSpinner\">\n                <h2>\n                    <i class=\"fa fa-spinner fa-spin\"></i>\n                </h2>\n            </div>\n            <div id=\"graph-symbol-price\"></div>\n        </div>\n    </div>\n</div>\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/candlesticks.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/market_status.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/terms.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"about\" %}\n{% set show_nab_bar = accepted_terms %}\n{% import 'macros/text.html' as m_text %}\n\n{% block body %}\n<br>\n\n<div class=\"card\">\n    <div class=\"card-header\"><h2>Disclaimer</h2></div>\n    <div class=\"card-body\">\n        {{ m_text.text_lines(disclaimer) }}\n        <div>\n            <a href=\"{{ url_for('accept_terms', accept_terms=True, next=url_for('welcome')) }}\" class=\"button btn btn-primary waves-effect\">\n                Accept and go to OctoBot\n            </a>\n            <button route=\"{{ url_for('commands', cmd='stop') }}\" type=\"button\" class=\"btn btn-danger waves-effect\">\n                Stop Octobot\n            </button>\n        </div>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n    <script src=\"{{ url_for('static', filename='js/components/commands.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/trading.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"trading\" %}\n{% set startup_messages_added_classes = \"justify-content-end px-4\" %}\n{% set inner_startup_messages_added_classes = \"offset-md-3 offset-lg-2 offset-1\" %}\n{% import 'macros/cards.html' as m_cards %}\n{% import 'macros/starting_waiter.html' as m_waiter %}\n\n{% set vars = {'exchange_overload': False} %}\n\n{% macro exchange_overload_warning(exchange, load) -%}\n    {% if load[\"has_reached_websocket_limit\"] %}\n        <div class=\"alert alert-info font-weight-normal offset-md-3 offset-lg-2 offset-1\" role=\"alert\">\n            <span class=\"text-capitalize\">{{ exchange }}</span> exchange websocket can't handle the required\n            {{load[\"load\"]}} simultaneous feed. Exchange maximum is {{load[\"max_load\"]}}. Falling back to the\n            slower \"REST\" connection.\n        </div>\n    {% elif load[\"overloaded\"] %}\n        <div class=\"alert alert-danger font-weight-normal offset-md-3 offset-lg-2 offset-1\" role=\"alert\">\n            <span class=\"text-capitalize\">{{ exchange }}</span> exchange is overloaded by\n            <span class=\"font-weight-bold\">{{ (load[\"load\"] * 100 / load[\"max_load\"] - 100) | round | int }}%</span>.\n            <span class=\"d-none d-sm-inline\">\n                <span class=\"text-capitalize\">{{ exchange }}</span> capacity is\n                <span class=\"font-weight-bold\">{{ load[\"max_load\"] }}</span> simultaneous traded pair/time frame\n                couples and the current configuration involves <span class=\"font-weight-bold\">{{ load[\"load\"] }}</span>.\n            </span>\n        </div>\n        {% set _ = vars.update({'exchange_overload': True}) %}\n    {% endif %}\n{%- endmacro %}\n\n{% macro waiter(waiter_id, title) %}\n<div id=\"{{waiter_id}}\" class=\"text-center my-4 py-4 h-100\">\n    <div class=\"py-4\">\n        <h2>{{title}}</h2>\n    </div>\n    <div class=\"py-4\">\n        <h2><i class=\"fa fa-spinner fa-spin\"></i></h2>\n    </div>\n</div>\n{% endmacro %}\n\n{% block body %}\n<br>\n\n{% if not (pairs_with_status or has_real_trader) %}\n    {{ m_waiter.display_loading_message(details=\"If this message remains, please make sure that at least one exchange is enabled in your profile.\") }}\n{% else %}\n    {% for exchange, load in exchanges_load.items() %}\n        {{ exchange_overload_warning(exchange, load) }}\n    {% endfor %}\n    {% if vars['exchange_overload'] %}\n    <div class=\"d-none d-md-block alert alert-warning font-weight-normal offset-md-3 offset-lg-2 offset-1\" role=\"alert\">\n        This limit is due to the exchange's REST API rate limit and prevents OctoBot from getting banned for spam.\n        OctoBot is operating with the given configuration but this exchange trading will be slowed down and might\n        underperform due to this rate limit restrictions.\n        Using a websocket tentacle for this exchange would handle as many pairs and time frames as wanted.\n    </div>\n    {% endif %}\n\n<div class=\"row\">\n    <nav class=\"mt-md-4 mt-2 col-md-3 col-lg-2 col-1 d-block sidebar shadow\">\n        <div class=\"sidebar-sticky mt-0 pt-0\">\n            <div class=\"nav flex-column pt-0 mt-0 mt-md-4\" id=\"v-tab\" role=\"tablist\"\n                 aria-orientation=\"vertical\">\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" data-tab={{ 'default' if has_pnl_history else 'other'}} id=\"panel-pnl-tab\" data-toggle=\"tab\"\n                   href=\"#panel-pnl\" role=\"tab\" aria-controls=\"panel-pnl\" aria-selected=\"true\">\n                    <i class=\"fa-solid fa-chart-line\"></i><span class=\"d-none d-md-block pl-3\">Profit & Loss</span>\n                </a>\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" data-tab={{ 'default' if not has_pnl_history else 'other'}} id=\"panel-orders-tab\" data-toggle=\"tab\"\n                   href=\"#panel-orders\" role=\"tab\" aria-controls=\"panel-orders\" aria-selected=\"false\">\n                    <i class=\"fa-solid fa-list-ul\"></i><span class=\"d-none d-md-block pl-3\">Orders</span>\n                </a>\n                {% if might_have_positions %}\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panel-positions-tab\" data-toggle=\"tab\"\n                   href=\"#panel-positions\" role=\"tab\" aria-controls=\"panel-positions\" aria-selected=\"false\">\n                    <i class=\"fa-solid fa-table-list\"></i><span class=\"d-none d-md-block pl-3\">Positions</span>\n                </a>\n                {% endif %}\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panel-market-status-tab\" data-toggle=\"tab\"\n                   href=\"#panel-market-status\" role=\"tab\" aria-controls=\"panel-market-status\" aria-selected=\"false\">\n                    <i class=\"fab fa-bitcoin\"></i><span class=\"d-none d-md-block pl-3\">Market Status</span>\n                </a>\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panel-trades-tab\" data-toggle=\"tab\"\n                   href=\"#panel-trades\" role=\"tab\" aria-controls=\"panel-trades\" aria-selected=\"false\">\n                    <i class=\"fa-solid fa-clock-rotate-left\"></i><span class=\"d-none d-md-block pl-3\">Trades</span>\n                </a>\n                {% if followed_strategy_url %}\n                <a class=\"nav-link pl-2 pl-sm-3 waves-effect d-flex\" id=\"panel-followed-strategy-tab\" data-toggle=\"tab\"\n                   href=\"#panel-followed-strategy\" role=\"tab\" aria-controls=\"panel-followed-strategy\" aria-selected=\"false\">\n                   <i class=\"fa-solid fa-copy\"></i><span class=\"d-none d-md-block pl-3\">Followed strategy</span>\n                </a>\n                {% endif %}\n            </div>\n        </div>\n    </nav>\n    <main role=\"main\" id=\"main-nav\" class=\"col-md-9 col-lg-10 col-11 ml-auto\">\n        <div class=\"tab-content\">\n            <div class=\"tab-pane fade\" id=\"panel-pnl\" role=\"tabpanel\"\n                 aria-labelledby=\"panel-pnl-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header d-flex justify-content-between\">\n                        <div>\n                            <h2>\n                                Profit and Loss\n                            </h2>\n                        </div>\n                        <div>\n                            <ul class=\"nav nav-pills\">\n                              <li class=\"nav-item\">\n                                <a class=\"nav-link primary-pill scale-selector\"\n                                   href=\"#\"\n                                   data-action=\"change-scale\"\n                                   data-scale=\"\"\n                                >Detailed</a>\n                              </li>\n                              <li class=\"nav-item\">\n                                <a class=\"nav-link primary-pill scale-selector\"\n                                   href=\"#\"\n                                   data-action=\"change-scale\"\n                                   data-scale=\"1h\"\n                                >1h</a>\n                              </li>\n                              <li class=\"nav-item\">\n                                <a class=\"nav-link primary-pill scale-selector\"\n                                   href=\"#\"\n                                   data-action=\"change-scale\"\n                                   data-scale=\"4h\"\n                                >4h</a>\n                              </li>\n                              <li class=\"nav-item\">\n                                <a class=\"nav-link primary-pill scale-selector active\"\n                                   href=\"#\"\n                                   data-action=\"change-scale\"\n                                   data-scale=\"1d\"\n                                >Daily</a>\n                              </li>\n                            </ul>\n                        </div>\n                    </div>\n                    <div class=\"card-body\">\n                        {{ waiter(\"pnl-waiter\", \"Loading Profit and Loss\") }}\n                        <div class=\"w-100\">\n                            {% if might_have_positions %}\n                            <div class=\"alert alert-info mt-4\">\n                                When trading futures, PNL might be incorrect. The team is working on fixing this issue.\n                            </div>\n                            {% endif %}\n                            <div class=\"d-flex justify-content-between\">\n                                <h4>\n                                    <span id=\"match-trades-count\" class=\"badge font-size-90 badge-success\"></span>\n                                    <span class=\"d-none d-md-inline\">Matched</span> Trades\n                                </h4>\n                                <select id=\"symbol-select\" class=\"selectpicker\" data-live-search=\"true\">\n                                    <option value=\"\" selected>\n                                        Reference market\n                                    </option>\n                                    {% for symbol in pnl_symbols | sort %}\n                                        <option value=\"{{ symbol }}\">\n                                            {{ symbol }}\n                                        </option>\n                                    {% endfor %}\n                                </select>\n                            </div>\n                            <div id=\"pnl_historyChart\"\n                                 data-url=\"{{url_for('api.pnl_history', quote=reference_market, scale='')}}\"\n                                 data-unit=\"{{reference_market}}\"\n                                 class=\"h-100 w-100\">\n                            </div>\n                        </div>\n                        <div class=\"w-100\">\n                            <table id=\"pnl_historyTable\"\n                                 data-unit=\"{{reference_market}}\"\n                                 class=\"w-100 table-striped table-responsive-sm\">\n                            </table>\n                        </div>\n                        <div class=\"alert alert-info mt-4\">\n                            For accuracy, Profit and Loss is only computed for trades that are coming from a\n                            trading mode that is supporting PNL history such as Grid Trading or the Dip Analyser.\n                            <br>\n                            Please check trading modes description to find out if PNL history is supported.\n                            Resetting the trades history will reset Profit and Loss.\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"tab-pane fade\" id=\"panel-orders\" role=\"tabpanel\"\n                 aria-labelledby=\"panel-orders-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\">\n                        <h2>Open orders\n                            <button type=\"button\" class=\"btn btn-outline-danger waves-effect float-right\" id=\"cancel_all_orders\"\n                                    update-url=\"{{ url_for('api.orders', action='cancel_orders') }}\"\n                                    disabled>\n                            <i id=\"cancel_all_icon\" class=\"fas fa-ban\"></i> <span class=\"d-none d-md-inline-block\">Cancel all</span>\n                            </button>\n                        </h2>\n                    </div>\n                    <div class=\"card-body\">\n                        {{ waiter(\"orders-waiter\", \"Loading orders\") }}\n                        <div class='progress mb-1' id='cancel_order_progress_bar' style='display: none;'>\n                            <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='100' aria-valuemin='0' aria-valuemax='100' style='width: 100%;'></div>\n                        </div>\n                        <div id=\"openOrderTable\">\n                          <table id=\"orders-table\"\n                                 data-url=\"{{url_for('api.orders')}}\"\n                                 data-cancel-url=\"{{url_for('api.orders', action='cancel_order')}}\"\n                                 class=\"w-100 table-striped table-responsive-sm\">\n                          </table>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {% if might_have_positions %}\n            <div class=\"tab-pane fade\" id=\"panel-positions\" role=\"tabpanel\"\n                 aria-labelledby=\"panel-positions-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\">\n                        <h2>Positions</h2>\n                    </div>\n                    <div class=\"card-body\">\n                        {{ waiter(\"positions-waiter\", \"Loading positions\") }}\n                        <div class='progress mb-1' id='cancel_position_progress_bar' style='display: none;'>\n                            <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='100' aria-valuemin='0' aria-valuemax='100' style='width: 100%;'></div>\n                        </div>\n                        <div id=\"positionTable\">\n                          <table id=\"positions-table\"\n                                 data-url=\"{{url_for('api.positions')}}\"\n                                 data-close-url=\"{{url_for('api.positions', action='close_position')}}\"\n                                 class=\"w-100 table-striped table-responsive-sm\">\n                          </table>\n                        </div>\n                    </div>\n                </div>\n                <br>\n            </div>\n            {% endif %}\n            <div class=\"tab-pane fade\" id=\"panel-market-status\" role=\"tabpanel\"\n                 aria-labelledby=\"panel-market-status-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\"><h2>Market status</h2></div>\n                    <div class=\"card-body\">\n                        <div class=\"card-deck\">\n                            {% for pair, status in pairs_with_status.items() | sort(attribute='0') %}\n                                {{ m_cards.pair_status_card(pair, status, watched_symbols, displayed_portfolio, symbols_values, reference_market) }}\n                            {% endfor %}\n\n                            <!-- Add new pair Card -->\n                            <div class=\"card mb-4 small-size unique-color-dark-card\">\n\n                                <!--Title-->\n                                <div class=\"card-header\">\n                                    <a class=\"\" href=\"{{ url_for('profile')+'#panelCurrency' }}\">New trading pair</a>\n                                </div>\n\n                                <!--Card image-->\n                                <div class=\"view overlay animated text-center pt-2 mb-5\">\n                                  <a href=\"{{ url_for('profile')+'#panelCurrency' }}\">\n                                    <i class=\"fas fa-8x fa-plus-circle fa-3x\"></i>\n                                  </a>\n                                </div>\n\n                                <div class=\"px-4 pt-3 pb-3\">\n                                    <!--Card content-->\n                                    <div class=\"list-group-flush text-center\">\n                                        <a class=\"btn btn-outline-primary hover_anim\"\n                                           href=\"{{ url_for('profile')+'#panelCurrency' }}\">\n                                            Add a new pair\n                                        </a>\n                                    </div>\n                                </div>\n\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"tab-pane fade\" id=\"panel-trades\" role=\"tabpanel\"\n                 aria-labelledby=\"panel-trades-tab\">\n                <div class=\"card\">\n                    <div class=\"card-header\"><h2>\n                        &ensp;Trades history\n                    </h2></div>\n                    <div class=\"card-body\">\n                    {{ waiter(\"trades-waiter\", \"Loading trades\") }}\n                      <table id=\"trades-table\"\n                             data-url=\"{{url_for('api.trades')}}\"\n                             data-reference-market=\"{{reference_market}}\"\n                             class=\"w-100 table-striped table-responsive-sm\">\n                      </table>\n                    </div>\n                </div>\n                <div class=\"d-flex justify-content-end\">\n                    <button\n                        data-url=\"{{ url_for('api.clear_trades_history') }}\"\n                        id=\"clear-trades-history-button\"\n                        class=\"btn btn-outline-warning waves-effect mt-2\">\n                        <i class=\"fas fa-trash\"></i> Clear trades history\n                    </button>\n                </div>\n            </div>\n            {% if followed_strategy_url %}\n            <div class=\"tab-pane fade\" id=\"panel-followed-strategy\" role=\"tabpanel\"\n                 aria-labelledby=\"panel-followed-strategy-tab\">\n                <div class=\"card\" role=\"alert\">\n                    <div class=\"card-header\">\n                        <h4>\n                            Subscribed to OctoBot trading signals\n                            {% if is_community_feed_connected %}\n                            <span class=\"badge badge-success\">Connected</span>\n                            {% else %}\n                            <span class=\"badge badge-danger\">Disconnected</span>\n                            {% endif %}\n                        </h4>\n                    </div>\n                    <div class=\"card-body\">\n                        {% if last_signal_time %}\n                        Last received signal: {{convert_timestamp(last_signal_time)}}.\n                        {% else %}\n                        No signal received yet.\n                        {% endif %}\n                        <div>\n                            Following <a target=\"_blank\" rel=\"noopener\" href=\"{{ followed_strategy_url }}\">{{ followed_strategy_url }}</a>.\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {% endif %}\n        </div>\n    </main>\n</div>\n    <div class=\"row\">\n    </div>\n    <br>\n    <span id=\"trading-orders-and-positions\">\n    </span>\n\n<div class=\"modal\" id=\"CancelAllOrdersModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#CancelAllOrdersModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n    <div class=\"modal-content modal-text\">\n      <div class=\"modal-header primary-text\">\n        <h5 class=\"modal-title\" id=\"#CancelAllOrdersModalLabel\">Confirm action</h5>\n        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n      </div>\n      <div class=\"modal-body text-center\">\n          <h4>Cancel all <strong id=\"ordersCount\"></strong> displayed orders ?</h4>\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" id=\"confirmCancelAllOrders\" class=\"btn btn-danger\" data-dismiss=\"modal\"><i class=\"fas fa-ban\"></i> Cancel orders</button>\n        <button type=\"button\" class=\"btn btn-primary\" data-dismiss=\"modal\">Close</button>\n      </div>\n    </div>\n  </div>\n</div>\n\n<div class=\"modal\" id=\"CancelOrderModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#CancelOrderModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n    <div class=\"modal-content modal-text\">\n      <div class=\"modal-header primary-text\">\n        <h5 class=\"modal-title\" id=\"#CancelOrderModalLabel\">Confirm action</h5>\n        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n      </div>\n      <div class=\"modal-body text-center\">\n          <h4>Cancel order ?</h4>\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" id=\"confirmCancelOrder\" class=\"btn btn-danger\"><i class=\"fas fa-ban\"></i> Cancel order</button>\n        <button type=\"button\" class=\"btn btn-primary\" data-dismiss=\"modal\">Close</button>\n      </div>\n    </div>\n  </div>\n</div>\n\n<div class=\"modal\" id=\"ClosePositionModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"#ClosePositionModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n    <div class=\"modal-content modal-text\">\n      <div class=\"modal-header primary-text\">\n        <h5 class=\"modal-title\" id=\"#ClosePositionModalLabel\">Confirm action</h5>\n        <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n      </div>\n      <div class=\"modal-body text-center\">\n          <h4>Close position ?</h4>\n          <p>\n              This will create orders to close this position.\n          </p>\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" class=\"btn btn-danger\"><i class=\"fas fa-ban\"></i> Close position</button>\n        <button type=\"button\" class=\"btn btn-primary\" data-dismiss=\"modal\">Close</button>\n      </div>\n    </div>\n  </div>\n</div>\n{% endif %}\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/custom_elements.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/pnl_history.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/tables_display.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/trading.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/trading_type_selector.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n{% set startup_messages_added_classes = \"d-none\" %}\n\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n{% import 'macros/starting_waiter.html' as m_starting_waiter %}\n{% import 'components/config/exchange_card.html' as m_config_exchange_card %}\n\n{% block body %}\n<br>\n<div class=\"card w-100 w-md-75 mx-auto\">\n    <div class=\"card-header d-flex justify-content-between\">\n        <div>\n            <h3>\n                <span class=\"d-none d-md-inline\">Final step: select how to trade using </span>\n                {{current_profile_name}}\n            </h3>\n        </div>\n    </div>\n    <div class=\"card-body pt-0 text-center\">\n        {{ m_flash_messages.flash_messages() }}\n        <div class=\"text-center my-2\">\n            Select the exchange to use :\n            <select id=\"AddExchangeSelect\" class=\"selectpicker\" data-live-search=\"true\">\n                <optgroup label=\"OctoBot fully tested\">\n                   {% for exchange in ccxt_tested_exchanges %}\n                        <option data-tokens=\"{{ exchange }}\" {{\"selected\" if exchange == enabled_exchanges[0] else \"\"}}>{{ exchange }}</option>\n                   {% endfor %}\n                </optgroup>\n                {% if ccxt_simulated_tested_exchanges %}\n                <optgroup label=\"OctoBot tested with simulated trading\">\n                   {% for exchange in ccxt_simulated_tested_exchanges %}\n                        <option data-tokens=\"{{ exchange }}\">{{ exchange }}</option>\n                   {% endfor %}\n                </optgroup>\n                {% endif %}\n                <optgroup label=\"OctoBot untested\">\n                   {% for exchange in ccxt_other_exchanges %}\n                        <option data-tokens=\"{{ exchange }}\">{{ exchange }}</option>\n                   {% endfor %}\n                </optgroup>\n            </select>\n        </div>\n        <div>\n            <ul class=\"nav nav-tabs md-tabs justify-content-center\" id=\"tabs\" role=\"tablist\">\n                <li class=\"nav-item\">\n                    <a class=\"nav-link primary-tab-selector {{'' if real_trader_activated else 'active show'}}\" id=\"simulated-tab\" data-toggle=\"tab\" href=\"#simulated\" role=\"tab\"\n                       aria-controls=\"simulated\"\n                       aria-selected=\"true\">\n                        <h5><i class=\"fa fa-robot\"></i> <span class=\"d-none d-md-inline\">Trade using</span> paper <span class=\"d-none d-md-inline\">money</span></h5>\n                    </a>\n                </li>\n                <li class=\"nav-item\">\n                    <a class=\"nav-link primary-tab-selector {{'active show' if real_trader_activated else ''}}\" id=\"real-tab\" data-toggle=\"tab\" href=\"#real\" role=\"tab\"\n                       aria-controls=\"real\"\n                       aria-selected=\"false\">\n                        <h5><i class=\"fa fa-coins\"></i> <span class=\"d-none d-md-inline\">Trade using</span> real <span class=\"d-none d-md-inline\">money</span></h5>\n                    </a>\n                </li>\n            </ul>\n        </div>\n        <div class=\"tab-content my-2\" id=\"exchanges-tab-content\"\n             data-exchange-name=\"{{enabled_exchanges[0]}}\" data-has-real-trader=\"{{real_trader_activated}}\">\n            <div class=\"tab-pane fade {{'' if real_trader_activated else 'active show'}}\" id=\"simulated\" role=\"tabsimulated\" aria-labelledby=\"simulated-tab\">\n                <div class=\"text-left\">\n                    <div data-role=\"exchange\" class=\"card mb-4 config-card\">\n\n                        <div class=\"card-header d-flex\" id=\"simulated-config-header\">\n                            <div class=\"col-7 col-lg-5\">\n                                <h4 class=\"text-capitalize\">\n                                    {{enabled_exchanges[0]}}\n                                </h4>\n                            </div>\n                            <div class=\"col-5 col-lg-5\">\n                                <a href=\"\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"waves-effect\">\n                                    <img class=\"img-fluid product-logo d-none\" src=\"\" alt=\"{{enabled_exchanges[0]}}-logo\" url=\"{{url_for('exchange_logo', name=enabled_exchanges[0])}}\">\n                                </a>\n                            </div>\n                        </div>\n\n                        <!--Card image-->\n                        <div class=\"view overlay\">\n                          <!--{{ exchange }}-->\n                        </div>\n\n                        <!--Card content-->\n                        <div class=\"card-body px-2 px-md-4 text-left\">\n                            <div id=\"portfolio-editor\"\n                                 data-portfolio='{{simulated_portfolio | tojson}}'\n                                 data-portfolio-schema='{{portfolio_schema | tojson}}'\n                                 data-currencies-url=\"{{url_for('api.get_all_currencies', exchange='')}}\"\n                            >\n\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div>\n                    <button class=\"btn btn-primary btn-lg\"\n                            data-toggle=\"tooltip\" data-placement=\"top\"\n                            title=\"Save and start trading with simulated money. Will restart OctoBot if necessary.\"\n                            data-role=\"start-trading\"\n                            data-trading-type=\"simulated\"\n                            data-config-url=\"{{url_for('config')}}\"\n                            data-start-url=\"{{url_for('wait_reboot', onboarding=onboarding, trading_delay_info=True, reboot='')}}\">\n                        Start trading\n                    </button>\n                </div>\n            </div>\n            <div class=\"tab-pane fade {{'active show' if real_trader_activated else ''}}\" id=\"real\" role=\"tabreal\" aria-labelledby=\"real-tab\">\n                <div id=\"exchange-container\" class=\"text-left\" update-url=\"{{url_for('api.are_compatible_accounts')}}\">\n                    {{ m_config_exchange_card.config_exchange_card(config_exchanges,\n                                                                   enabled_exchanges[0],\n                                                                   exchanges_details[enabled_exchanges[0]],\n                                                                   is_supporting_future_trading,\n                                                                   enabled=True,\n                                                                   sandboxed=False,\n                                                                   selected_exchange_type=config_exchanges[enabled_exchanges[0]].get('exchange-type', 'spot'),\n                                                                   full_config=True,\n                                                                   lite_config=True)}}\n                </div>\n                <div>\n                    <button class=\"btn btn-primary btn-lg\"\n                            data-toggle=\"tooltip\" data-placement=\"top\"\n                            title=\"Save and start trading with exchange funds. Will restart OctoBot if necessary.\"\n                            data-role=\"start-trading\"\n                            data-trading-type=\"real\"\n                            data-config-url=\"{{url_for('config')}}\"\n                            data-start-url=\"{{url_for('wait_reboot', onboarding=onboarding, trading_delay_info=True, reboot='')}}\">\n                        Start trading\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n\n<br>\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/common_handlers.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/exchange_accounts.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/trading_type_selector.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/tradingview_email_config.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"accounts\" %}\n{% import \"components/community/tentacle_packages.html\" as m_tentacle_packages %}\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n\n{% macro tutorial_article(title, image, details) -%}\n<div class=\"my-2\">\n    <h4>\n        {{ title | safe }}\n    </h4>\n    {% if image %}\n    <div class=\"text-center mt-3\">\n        <img src=\"{{url_for('static', filename=image)}}\" class=\"img-fluid interface-screen\" alt=\"tutorial illustration\">\n    </div>\n    {% endif %}\n    <p class=\"pt-2 text-center\">\n        {{ details | safe }}\n    </p>\n</div>\n{%- endmacro %}\n\n{% block body %}\n<br>\n\n{{ m_flash_messages.flash_messages() }}\n\n\n<div class=\"card w-100 w-lg-50 mx-auto\">\n    <div class=\"card-header\">\n        <h2>\n            Configure OctoBot to trade using TradingView email alerts\n        </h2>\n    </div>\n    <div class=\"card-body\">\n        {% if not is_community_authenticated %}\n        <div class=\"text-center\">\n            <p>\n                Please login to your OctoBot account to configure your TradingView email alerts.\n            </p>\n            <a type=\"button\" class=\"btn btn-primary btn-lg my-4\"\n               href=\"{{ url_for('community_login', next='tradingview_email_config') }}\">\n                Login\n            </a>\n            <p>\n                Note: The <a class=\"font-weight-bolder\" href=\"{{url_for('extensions')}}\">{{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}</a>\n                is required to connect your OctoBot to TradingView email alerts.\n            </p>\n        </div>\n        {% elif has_open_source_package() %}\n            <div id=\"config-stepper\"\n                 data-trigger-verif-code-waiter=\"{{url_for('api.trigger_wait_for_email_address_confirm_code_email')}}\"\n                 data-get-verif-code-content=\"{{url_for('api.tradingview_confirm_email_content')}}\"\n            >\n                 <div class=\"card-body pt-1 tutorial-step\" data-step-id=\"1\">\n                    {{ tutorial_article(\n                        \"Trading using email alerts\",\n                        \"img/tradingview/tradingview-logo.png\",\n                        \"Follow those steps to add your OctoBot TradingView email address to your TradingView account \"\n                     \"if your TradingView alert email is <span class='font-weight-bolder'>unset or different</span> from: <p\n                            class='font-weight-bolder text-center pointer-cursor'\n                            data-role='copy-to-clipboard' data-name='Email address' data-value='\" + tradingview_email_address + \"'\n                            data-toggle='tooltip' data-placement='bottom' title='Click to copy'>\"+ tradingview_email_address + \"</p>\"\n                    ) }}\n                </div>\n                <div class=\"card-body pt-1 tutorial-step d-none\" data-step-id=\"2\">\n                    {{ tutorial_article(\n                        \"1. Create or edit an alert\",\n                        \"img/tradingview/tradingview-create-alert.png\",\n                        \"Create / edit an alert to open the alert configuration view\"\n                    ) }}\n                    {{ tutorial_article(\n                        \"\",\n                        \"img/tradingview/create-alert-view.png\",\n                        \"\"\n                    ) }}\n                </div>\n                <div class=\"card-body pt-1 tutorial-step d-none\" data-step-id=\"3\">\n                    {{ tutorial_article(\n                        \"2. Go to the Notifications tab and select Send plain text\",\n                        \"img/tradingview/tradingview-alert-notification-email-selected-form.png\",\n                        \"Go to the Notifications tab and select <span class='font-weight-bolder'>Send plain text</span>.\n                        <br/>This will open the 'Account verification' modal.\n                        <p class='mt-4'>Note: if the modal doesn't show up, it means that an alert email address is already set.\n                        In this case, go to your <span class='font-weight-bolder'>profile settings</span> and edit your\n                            <span class='font-weight-bolder'>Alternative email for alerts</span>.</p>\"\n                    ) }}\n                </div>\n                <div class=\"card-body pt-1 tutorial-step d-none\" data-step-id=\"4\">\n                    {{ tutorial_article(\n                        \"3. Enter your OctoBot alert email address\",\n                        \"img/tradingview/tradingview-alert-email-form.png\",\n                        \"Enter your OctoBot alert email address: <p\n                            class='font-weight-bolder text-center pointer-cursor'\n                            data-role='copy-to-clipboard' data-name='Email address' data-value='\" + tradingview_email_address + \"'\n                            data-toggle='tooltip' data-placement='bottom' title='Click to copy'>\"+ tradingview_email_address + \"</p>\n                            <p class='text-center'>and click 'Get code'.</p>\"\n                    ) }}\n                </div>\n                <div class=\"card-body pt-1 tutorial-step d-none\" data-step-id=\"5\">\n                    {{ tutorial_article(\n                        \"4. Enter your verification code\",\n                        \"img/tradingview/tradingview-alert-email-form-confirm-code.png\",\n                        \"<span data-role='verification-code-waiter'>Email address: <span\n                            class='font-weight-bolder pointer-cursor'\n                            data-role='copy-to-clipboard' data-name='Email address' data-value='\" + tradingview_email_address + \"'\n                            data-toggle='tooltip' data-placement='bottom' title='Click to copy'>\"+ tradingview_email_address + \"</span><br/>\n                            ✅ Your verification code is on the way, it will be displayed here ...</span>\n                         <span class='d-none' data-role='verification-code-received'>Enter your verification code:</span>\n                         <p class='text-center mt-4'> <span data-role='verification-code-waiter'>\n                         <i class='fa fa-spinner fa-spin fa-2xl my-4'></i><br/>\n                         Receiving the code may take up to 2 minute.\n                         </span><span class='d-none font-weight-bolder' id='verification-code-received-content' data-role='verification-code-received'></span>\n                         <span class='d-none' data-role='verification-code-error'>😦 Receiving the code is taking too long.\n                         Can you please double-check the email address?<br/>\n                         Email address: <span\n                         class='font-weight-bolder pointer-cursor'\n                         data-role='copy-to-clipboard' data-name='Email address' data-value='\" + tradingview_email_address + \"'\n                         data-toggle='tooltip' data-placement='bottom' title='Click to copy'>\"+ tradingview_email_address + \"</span><br/>\n                         <span class='font-weight-bolder' id='verification-code-error-content'></span><br/>Please\n                         <a href='mailto:contact@octobot.cloud?subject=Open source TradingView email alert config issue'>\n                         contact the support</a> if your believe this is an issue with OctoBot.</span></p>\"\n                    ) }}\n                </div>\n                <div class=\"card-body pt-1 tutorial-step d-none\" data-step-id=\"6\">\n                    {{ tutorial_article(\n                        \"You are all set!\",\n                        \"img/tradingview/tradingview-alert-email-form-completed.png\",\n                        \"🎉 TradingView will now notify your OctoBot using emails when your alerts will fire.\"\n                    ) }}\n                    {{ tutorial_article(\n                        \"Last words\",\n                        \"img/tradingview/use-email-alerts.png\",\n                        \"<ul><li>Remember to check <span class='font-weight-bolder'>Use-Email-Alerts</span> in your <a href='/accounts#panelServices'>TradingView interface configuration</a> to\n                        make your OctoBot listen to email alerts.</li>\n                        <li>Selecting a TradingView-related profile such as 'TradingView Signals Trading' is necessary to trade using TradingView on OctoBot.</li><li>Avoid enabling both email and webhook alerts on TradingView otherwise alerts will trigger\n                        twice your OctoBot.</li></ul> <p class='text-center'><a type='button' class='btn btn-primary'\n                        href='accounts#panelServices'>Back to TradingView configuration</a></p>\"\n                    ) }}\n                </div>\n            </div>\n        {% else %}\n            <div class=\"alert alert-info\">\n                Using the <a class=\"font-weight-bolder\" href=\"{{url_for('extensions')}}\">{{OCTOBOT_EXTENSION_PACKAGE_1_NAME}}</a>,\n                your OctoBot can trade using TradingView free email alerts.\n            </div>\n            <div class=\"text-center\">\n                <a type=\"button\" class=\"btn btn-primary btn-lg\"\n                   href=\"{{ url_for('extensions') }}\">\n                    View extension\n                </a>\n            </div>\n        {% endif %}\n    </div>\n    {% if is_community_authenticated and has_open_source_package() %}\n    <div class=\"card-footer\">\n        <div class=\"w-75 mx-auto d-sm-block d-md-none pb-2\">\n            <div class='progress'>\n                <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='0' aria-valuemin='0' aria-valuemax='100' style='width: 0%;'></div>\n            </div>\n        </div>\n        <div class=\"d-flex justify-content-between flex-wrap\">\n            <div class=\"\">\n                <button id=\"previous-step\" class=\"btn btn-outline-primary\">\n                    <i class=\"fas fa-arrow-left\"></i>\n                </button>\n            </div>\n            <div class=\"row w-75 my-auto d-none d-md-flex\">\n                <div class=\"offset-md-1 col-md-2\">\n                    Progress:\n                </div>\n                <div class='progress col-md-8 my-auto px-0'>\n                    <div class='progress-bar progress-bar-striped progress-bar-animated' role='progressbar' aria-valuenow='0' aria-valuemin='0' aria-valuemax='100' style='width: 0%;'></div>\n                </div>\n            </div>\n            <div class=\"\">\n                <button id=\"next-step\" class=\"btn btn-primary\">\n                    <i class=\"fas fa-arrow-right\"></i>\n                </button>\n            </div>\n        </div>\n    </div>\n    {% endif %}\n</div>\n\n<br>\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/common/stepper.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/components/tradingview_email_config.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n<script src=\"{{ url_for('static', filename='js/common/resources_rendering.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n\n<script>\n    let stepperCallbackById = {\n        1: triggerEmailConfirmWaiter\n    }\n</script>\n\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/wait_reboot.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"profile\" %}\n\n{% import 'macros/flash_messages.html' as m_flash_messages %}\n{% import 'macros/starting_waiter.html' as m_starting_waiter %}\n{% import 'components/config/exchange_card.html' as m_config_exchange_card %}\n\n{% block body %}\n<br>\n{{ m_flash_messages.flash_messages() }}\n<div class=\"mb-2\" id=\"restart-loader\"\n     data-redirect-url=\"{{next_url}}\">\n{{ m_starting_waiter.display_loading_message(\"Your OctoBot is restarting using the \" + current_profile_name + \" profile.\",\n                                             \"You will be taken to your OctoBot dashboard when it will be ready.\",\n                                             next_url=next_url)}}\n</div>\n\n\n<br>\n\n{% endblock %}\n\n{% block additional_scripts %}\n<script src=\"{{ url_for('static', filename='js/components/wait_reboot.js', u=LAST_UPDATED_STATIC_FILES) }}\"></script>\n{% endblock additional_scripts %}"
  },
  {
    "path": "Services/Interfaces/web_interface/templates/welcome.html",
    "content": "{% extends \"layout.html\" %}\n{% set active_page = \"home\" %}\n{% set show_nab_bar = false %}\n\n{% block body %}\n<br>\n\n<div class=\"card w-100 w-md-75 w-lg-50 mx-auto\">\n    <div class=\"card-header text-center\"><h2>Welcome to your OctoBot !</h2></div>\n    <div class=\"card-body\">\n        <div>\n            <p>\n                To quickly get started with OctoBot, the first thing to do is to select a strategy to use.\n            </p>\n            <p>\n                A strategy is defined in a profile. Each profile can be used either with your real exchange\n                account or using paper trading. Paper trading\n                allows you to experiment a profile risk-free, with a virtual portfolio.\n            </p>\n        </div>\n        <div class=\"text-center\">\n            <img class=\"img-fluid cloud-logo-4x\"\n                 src=\"{{url_for('static', filename='img/community/cloud_dark.png')}}\" alt=\"OctoBot cloud\">\n        </div>\n        <div>\n            How do you want to first start your OctoBot ? Remember that you can always change your mind later on.\n        </div>\n    </div>\n    <div class=\"card-footer p-4\">\n        <div>\n            <div class=\"row\">\n                <div class=\"col col-xl-8\">\n                    <h3>OctoBot cloud strategies</h3>\n                    <p>\n                        I first want to use ready-made strategies from\n                    <a href=\"{{OCTOBOT_COMMUNITY_URL}}?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=welcome\" target=\"_blank\"> OctoBot cloud\n                        <i class=\"fas fa-external-link-alt\"></i></a>.\n                    </p>\n                    <p>\n                        <i class=\"far fa-lightbulb\"></i> You can also use OctoBot cloud\n                        </a> for free to easily run cloud strategies.\n                    </p>\n                </div>\n                <div class=\"col my-auto\">\n                    <a href=\"{{ url_for('accept_terms', accept_terms=True, next=url_for('profiles_selector', use_cloud=True, onboarding=True)) }}\"\n                       class=\"button btn btn-primary waves-effect\">\n                        Use cloud strategies\n                    </a>\n                </div>\n            </div>\n            <div class=\"row border-top mt-4\">\n                <div class=\"col col-xl-8 mt-4\">\n                    <h3>Strategies to customize</h3>\n                    <p>\n                        I want to create and customize my own strategies.\n                    </p>\n                    <p>\n                        <i class=\"fas fa-user-graduate\"></i> For advanced investors.\n                    </p>\n                </div>\n                <div class=\"col mt-4 my-auto\">\n                    <a href=\"{{ url_for('accept_terms', accept_terms=True, next=url_for('profiles_selector', use_cloud=false, onboarding=True)) }}\"\n                       class=\"button btn btn-outline-primary waves-effect\">\n                        Use custom strategies\n                    </a>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n\n<br>\n{% endblock %}\n"
  },
  {
    "path": "Services/Interfaces/web_interface/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport threading\nimport asyncio\nimport time\nimport mock\nimport contextlib\n\nimport octobot_commons.configuration as configuration\nimport octobot_commons.singleton as singleton\nimport octobot_commons.authentication as authentication\nimport octobot_commons.constants as commons_constants\n\nimport octobot_services.interfaces as interfaces\nimport octobot.community as community\ntry:\n    import octobot.community.supabase_backend.configuration_storage as configuration_storage\nexcept ImportError:\n    # todo remove once supabase migration is complete\n    configuration_storage = mock.Mock(\n        ASyncConfigurationStorage=mock.Mock(\n            _save_value_in_config=mock.Mock()\n        )\n    )\nimport octobot.automation as automation\nimport octobot.enums\nimport octobot_commons.constants\n\nimport tentacles.Services.Interfaces.web_interface.controllers.octobot_authentication as octobot_authentication\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface as web_interface\n\n\nPORT = 5555\nPASSWORD = \"123\"\nMAX_START_TIME = 5\nNON_AUTH_ROUTES = [\"/api/\", \"robots.txt\"]\n\n\nasync def _init_bot(distribution: octobot.enums.OctoBotDistribution):\n    # import here to prevent web interface import issues\n    import octobot.octobot as octobot\n    import octobot.constants as octobot_constants\n    import octobot.producers as producers\n    import octobot_commons.tests as test_config\n    import octobot_tentacles_manager.loaders as loaders\n    import octobot_evaluators.api as evaluators_api\n    import tests.test_utils.config as config\n    # force community CommunityAuthentication reset\n    community.IdentifiersProvider.use_production()\n    singleton.Singleton._instances.pop(authentication.Authenticator, None)\n    singleton.Singleton._instances.pop(community.CommunityAuthentication, None)\n    test_config = test_config.load_test_config(dict_only=False)\n    test_config.config[octobot_commons.constants.CONFIG_DISTRIBUTION] = distribution.value\n    octobot = octobot.OctoBot(test_config)\n    octobot.initialized = True\n    tentacles_config = config.load_test_tentacles_config()\n    loaders.reload_tentacle_by_tentacle_class()\n    octobot.task_manager.async_loop = asyncio.get_event_loop()\n    octobot.task_manager.create_pool_executor()\n    octobot.tentacles_setup_config = tentacles_config\n    octobot.configuration_manager.add_element(octobot_constants.TENTACLES_SETUP_CONFIG_KEY, tentacles_config)\n    octobot.exchange_producer = producers.ExchangeProducer(None, octobot, None, False)\n    octobot.evaluator_producer = producers.EvaluatorProducer(None, octobot)\n    await evaluators_api.initialize_evaluators(octobot.config, tentacles_config)\n    octobot.evaluator_producer.matrix_id = evaluators_api.create_matrix()\n    # Do not edit config file\n    octobot.community_auth.edited_config = None\n    octobot.automation = automation.Automation(octobot.bot_id, tentacles_config)\n    return octobot\n\n\ndef _start_web_interface(interface):\n    asyncio.run(interface.start())\n\n\n# use context manager instead of fixture to prevent pytest threads issues\n@contextlib.asynccontextmanager\nasync def get_web_interface(require_password: bool, distribution: octobot.enums.OctoBotDistribution):\n    web_interface_instance = None\n    try:\n        with mock.patch.object(configuration_storage.SyncConfigurationStorage, \"_save_value_in_config\", mock.Mock()):\n            web_interface_instance = web_interface.WebInterface({})\n            web_interface_instance.port = PORT\n            web_interface_instance.should_open_web_interface = False\n            web_interface_instance.set_requires_password(require_password)\n            web_interface_instance.password_hash = configuration.get_password_hash(PASSWORD)\n            bot = await _init_bot(distribution)\n            interfaces.AbstractInterface.bot_api = bot.octobot_api\n            first_exchange = next(iter(bot.config[commons_constants.CONFIG_EXCHANGES]))\n            with mock.patch.object(web_interface_instance, \"_register_on_channels\", new=mock.AsyncMock()), \\\n                 mock.patch.object(models, \"get_current_exchange\", mock.Mock(return_value=first_exchange)):\n                threading.Thread(target=_start_web_interface, args=(web_interface_instance,)).start()\n                # ensure web interface had time to start or it can't be stopped at the moment\n                launch_time = time.time()\n                while not web_interface_instance.started and time.time() - launch_time < MAX_START_TIME:\n                    await asyncio.sleep(0.3)\n                if not web_interface_instance.started:\n                    raise RuntimeError(\"Web interface did not start in time\")\n                yield web_interface_instance\n    finally:\n        if web_interface_instance is not None:\n            await web_interface_instance.stop()\n\n\nasync def check_page_no_login_redirect(url, session):\n    COMMUNITY_LOGIN_CONTAINED_PAGE_SUFFIXES = [\n        \"login\", \"logout\", \"/profiles_selector\",\n        \"/community\"  # redirects\n    ]\n    async with session.get(url) as resp:\n        text = await resp.text()\n        assert \"We are sorry, but an unexpected error occurred\" not in text, f\"{url=}\"\n        assert \"We are sorry, but this doesn't exist\" not in text, f\"{url=}\"\n        if not (any(url.endswith(suffix)) for suffix in COMMUNITY_LOGIN_CONTAINED_PAGE_SUFFIXES):\n            assert \"input type=submit value=Login\" not in text, f\"{url=}\"\n            assert not resp.real_url.name == \"login\", f\"{resp.real_url.name=} != 200 ({url=})\"\n        assert resp.status == 200, f\"{resp.status=} != 200 ({url=})\"\n\n\nasync def check_page_login_redirect(url, session):\n    async with session.get(url) as resp:\n        text = await resp.text()\n        assert \"We are sorry, but an unexpected error occurred\" not in text, f\"{url=}\"\n        assert \"We are sorry, but this doesn't exist\" not in text, f\"{url=}\"\n        if not any(route in url for route in NON_AUTH_ROUTES):\n            assert \"input type=submit value=Login\" in text, url\n            assert resp.real_url.name == \"login\", f\"{resp.real_url.name=} != 200 ({url=})\"\n        assert resp.status == 200, f\"{resp.status=} != 200 ({url=})\"\n\ndef get_plugins_routes(web_interface_instance):\n    all_rules = tuple(rule for rule in web_interface_instance.server_instance.url_map.iter_rules())\n    plugin_routes = []\n    for plugin in web_interface_instance.registered_plugins:\n        plugin_routes += [\n            rule.rule\n            for rule in get_plugin_routes(web_interface_instance.server_instance, plugin, all_rules)\n        ]\n    return plugin_routes\n\n\ndef get_plugin_routes(app, plugin, all_rules=None):\n    all_rules = all_rules or [rule for rule in app.url_map.iter_rules()]\n    return (\n        route for route in all_rules\n        if route.rule.startswith(f\"{plugin.blueprint.url_prefix}/\")\n    )\n\n\ndef _force_validate_on_submit(*_):\n    return True\n\n\nasync def login_user_on_session(session):\n    login_data = {\n        \"password\": PASSWORD,\n        \"remember_me\": False\n    }\n    with mock.patch.object(octobot_authentication.LoginForm, \"validate_on_submit\", new=_force_validate_on_submit):\n        async with session.post(f\"http://localhost:{PORT}/login\",\n                                data=login_data) as resp:\n            assert resp.status == 200\n\n\ndef get_all_plugin_rules(app, plugin_class, black_list):\n    plugin_instance = plugin_class.factory()\n    plugin_instance.blueprint_factory()\n    return set(rule.rule\n               for rule in get_plugin_routes(app, plugin_instance)\n               if \"GET\" in rule.methods\n               and _has_no_empty_params(rule)\n               and rule.rule not in black_list)\n\n\ndef _has_no_empty_params(rule):\n    defaults = rule.defaults if rule.defaults is not None else ()\n    arguments = rule.arguments if rule.arguments is not None else ()\n    return len(defaults) >= len(arguments)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/tests/distribution_tester.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport aiohttp\nimport asyncio\n\nimport tentacles.Services.Interfaces.web_interface.tests as web_interface_tests\nimport octobot.enums\n\n\nLOCAL_HOST_URL = \"http://localhost:\"\n\n\nclass AbstractDistributionTester:\n    VERBOSE = False  # Set true to print tested urls\n    DISTRIBUTION: octobot.enums.OctoBotDistribution = None\n    # backlist endpoints expecting additional data\n    URL_BLACK_LIST = []\n    DOTTED_URLS = []\n\n\n    async def test_browse_all_pages_no_required_password(self):\n        await self._inner_test_browse_all_pages_no_required_password([])\n\n\n    async def _inner_test_browse_all_pages_no_required_password(self, black_list: list[str]):\n        async with web_interface_tests.get_web_interface(False, self.DISTRIBUTION) as web_interface_instance:\n            async with aiohttp.ClientSession() as session:\n                await asyncio.gather(*[\n                    web_interface_tests.check_page_no_login_redirect(self._get_rule_url(rule), session)\n                    for rule in self._get_all_native_rules(web_interface_instance, black_list=black_list)\n                ])\n\n    async def test_browse_all_pages_required_password_without_login(self):\n        await self._inner_test_browse_all_pages_required_password_without_login([])\n\n    async def _inner_test_browse_all_pages_required_password_without_login(self, black_list: list[str]):\n        async with web_interface_tests.get_web_interface(True, self.DISTRIBUTION) as web_interface_instance:\n            async with aiohttp.ClientSession() as session:\n                await asyncio.gather(*[\n                    web_interface_tests.check_page_login_redirect(self._get_rule_url(rule), session)\n                    for rule in self._get_all_native_rules(web_interface_instance, black_list=black_list)\n                ])\n\n    async def test_browse_all_pages_required_password_with_login(self):\n        await self.inner_test_browse_all_pages_required_password_with_login([], [])\n\n    async def inner_test_browse_all_pages_required_password_with_login(\n            self, auth_black_list: list[str], unauth_black_list: list[str]\n    ):\n        async with web_interface_tests.get_web_interface(True, self.DISTRIBUTION) as web_interface_instance:\n            async with aiohttp.ClientSession() as session:\n                await web_interface_tests.login_user_on_session(session)\n                # correctly display pages: session is logged in\n                await asyncio.gather(*[\n                    web_interface_tests.check_page_no_login_redirect(self._get_rule_url(rule), session)\n                    for rule in self._get_all_native_rules(web_interface_instance, black_list=auth_black_list)\n                ])\n            async with aiohttp.ClientSession() as unauthenticated_session:\n                # redirect to login page: session is not logged in\n                await asyncio.gather(*[\n                    web_interface_tests.check_page_login_redirect(self._get_rule_url(rule), unauthenticated_session)\n                    for rule in self._get_all_native_rules(web_interface_instance, black_list=unauth_black_list)\n                ])\n\n    async def test_logout(self):\n        async with web_interface_tests.get_web_interface(True, self.DISTRIBUTION):\n            async with aiohttp.ClientSession() as session:\n                await web_interface_tests.login_user_on_session(session)\n                await web_interface_tests.check_page_no_login_redirect(\n                    f\"{LOCAL_HOST_URL}{web_interface_tests.PORT}/\", session\n                )\n                await web_interface_tests.check_page_login_redirect(\n                    f\"{LOCAL_HOST_URL}{web_interface_tests.PORT}/logout\",\n                    session)\n                await web_interface_tests.check_page_login_redirect(\n                    f\"{LOCAL_HOST_URL}{web_interface_tests.PORT}/\", session\n                )\n\n    def _get_all_native_rules(self, web_interface_instance, black_list=None):\n        if black_list is None:\n            black_list = []\n        full_back_list = self.URL_BLACK_LIST + black_list + web_interface_tests.get_plugins_routes(web_interface_instance)\n        rules = set(\n            rule.rule\n            for rule in web_interface_instance.server_instance.url_map.iter_rules()\n            if \"GET\" in rule.methods\n            and _has_no_empty_params(rule)\n            and rule.rule not in full_back_list\n        )\n        if self.VERBOSE:\n            print(f\"{self.__class__.__name__} Tested {len(rules)} rules: {rules}\")\n        return rules\n\n    def _get_rule_url(self, rule: str):\n        if rule in self.DOTTED_URLS:\n            path = rule\n        else:\n            path = rule.replace('.', '/')\n        return f\"{LOCAL_HOST_URL}{web_interface_tests.PORT}{path}\"\n\n\ndef _has_no_empty_params(rule):\n    defaults = rule.defaults if rule.defaults is not None else ()\n    arguments = rule.arguments if rule.arguments is not None else ()\n    return len(defaults) >= len(arguments)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/tests/distributions/__init__.py",
    "content": ""
  },
  {
    "path": "Services/Interfaces/web_interface/tests/distributions/test_default.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport tentacles.Services.Interfaces.web_interface.tests.distribution_tester as distribution_tester\nimport octobot.enums\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n_COMMUNITY_ACCOUNT_REQUIRED_PATHS = [\n    \"/advanced/tentacles\",\n    \"/advanced/tentacle_packages\",\n    \"/api/tradingview_confirm_email_content\",\n]\n\n\nclass TestDefaultDistribution(distribution_tester.AbstractDistributionTester):\n    DISTRIBUTION = octobot.enums.OctoBotDistribution.DEFAULT\n    # backlist endpoints expecting additional data\n    URL_BLACK_LIST = [\n        \"/symbol_market_status\", \"/tentacle_media\", \"/watched_symbols\", \"/export_logs\", \"/api/first_exchange_details\"\n    ]\n    DOTTED_URLS = [\"/robots.txt\"]\n    VERBOSE = False\n\n    async def test_browse_all_pages_no_required_password(self):\n        await self._inner_test_browse_all_pages_no_required_password(_COMMUNITY_ACCOUNT_REQUIRED_PATHS)\n\n    async def test_browse_all_pages_required_password_without_login(self):\n        await self._inner_test_browse_all_pages_required_password_without_login([])\n\n    async def test_browse_all_pages_required_password_with_login(self):\n        await self.inner_test_browse_all_pages_required_password_with_login(_COMMUNITY_ACCOUNT_REQUIRED_PATHS, [])\n"
  },
  {
    "path": "Services/Interfaces/web_interface/tests/distributions/test_market_making.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport tentacles.Services.Interfaces.web_interface.tests.distribution_tester as distribution_tester\nimport octobot.enums\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n_COMMUNITY_ACCOUNT_REQUIRED_PATHS = [\n    \"/api/tradingview_confirm_email_content\",\n]\n\n\nclass TestMarketMakingDistributionPlugin(distribution_tester.AbstractDistributionTester):\n    DISTRIBUTION = octobot.enums.OctoBotDistribution.MARKET_MAKING\n    # backlist endpoints expecting additional data\n    URL_BLACK_LIST = [\n        \"/tentacle_media\", \"/export_logs\", \"/api/first_exchange_details\"\n    ]\n    DOTTED_URLS = [\"/robots.txt\"]\n    VERBOSE = False\n\n    async def test_browse_all_pages_no_required_password(self):\n        await self._inner_test_browse_all_pages_no_required_password(_COMMUNITY_ACCOUNT_REQUIRED_PATHS)\n\n    async def test_browse_all_pages_required_password_without_login(self):\n        await self._inner_test_browse_all_pages_required_password_without_login([])\n\n    async def test_browse_all_pages_required_password_with_login(self):\n        await self.inner_test_browse_all_pages_required_password_with_login(\n            _COMMUNITY_ACCOUNT_REQUIRED_PATHS, []\n        )\n"
  },
  {
    "path": "Services/Interfaces/web_interface/tests/plugin_tester.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport aiohttp\nimport asyncio\n\nimport tentacles.Services.Interfaces.web_interface.tests as web_interface_tests\nimport octobot.enums\n\n\nclass AbstractPluginTester:\n    DISTRIBUTION: octobot.enums.OctoBotDistribution = octobot.enums.OctoBotDistribution.DEFAULT\n    VERBOSE = False  # Set true to print tested urls\n    PLUGIN = None\n    URL_BLACK_LIST = []\n\n    async def test_browse_all_pages_no_required_password(self):\n        async with web_interface_tests.get_web_interface(False, self.DISTRIBUTION) as web_interface_instance:\n            async with aiohttp.ClientSession() as session:\n                await asyncio.gather(\n                    *[web_interface_tests.check_page_no_login_redirect(\n                        f\"http://localhost:{web_interface_tests.PORT}{rule.replace('.', '/')}\",\n                        session)\n                        for rule in self._get_rules(web_interface_instance)])\n\n    async def test_browse_all_pages_required_password_without_login(self):\n        async with web_interface_tests.get_web_interface(True, self.DISTRIBUTION) as web_interface_instance:\n            async with aiohttp.ClientSession() as session:\n                await asyncio.gather(\n                    *[web_interface_tests.check_page_login_redirect(\n                        f\"http://localhost:{web_interface_tests.PORT}{rule.replace('.', '/')}\",\n                        session)\n                        for rule in self._get_rules(web_interface_instance)])\n\n    async def test_browse_all_pages_required_password_with_login(self):\n        async with web_interface_tests.get_web_interface(True, self.DISTRIBUTION) as web_interface_instance:\n            async with aiohttp.ClientSession() as session:\n                await web_interface_tests.login_user_on_session(session)\n                # correctly display pages: session is logged in\n                await asyncio.gather(\n                    *[web_interface_tests.check_page_no_login_redirect(\n                        f\"http://localhost:{web_interface_tests.PORT}{rule.replace('.', '/')}\",\n                        session)\n                        for rule in self._get_rules(web_interface_instance)])\n            async with aiohttp.ClientSession() as unauthenticated_session:\n                # redirect to login page: session is not logged in\n                await asyncio.gather(\n                    *[web_interface_tests.check_page_login_redirect(\n                        f\"http://localhost:{web_interface_tests.PORT}{rule.replace('.', '/')}\",\n                        unauthenticated_session)\n                        for rule in self._get_rules(web_interface_instance)])\n\n    def _get_rules(self, web_interface_instance):\n        rules = web_interface_tests.get_all_plugin_rules(\n            web_interface_instance.server_instance,\n            self.PLUGIN,\n            self.URL_BLACK_LIST\n        )\n        if self.VERBOSE:\n            print(f\"{self.__class__.__name__} Tested {len(rules)} rules: {rules}\")\n        return rules\n"
  },
  {
    "path": "Services/Interfaces/web_interface/util/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom tentacles.Services.Interfaces.web_interface.util import flask_util\nfrom tentacles.Services.Interfaces.web_interface.util.flask_util import (\n    get_rest_reply,\n)\nfrom tentacles.Services.Interfaces.web_interface.util import browser_util\nfrom tentacles.Services.Interfaces.web_interface.util.browser_util import (\n    open_in_background_browser,\n)\n\n__all__ = [\n    \"get_rest_reply\",\n    \"open_in_background_browser\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/util/browser_util.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport os\nimport webbrowser\n\n\ndef open_in_background_browser(url):\n    \"\"\"\n    Uses webbrowser.open(url) but skips non-background browsers as they are blocking the current process,\n    we don't want that.\n    Warning: should be called before any other call to webbrowser otherwise default browser discovery\n    (including non-background browsers) will be processed by webbrowser\n    \"\"\"\n    # env var used to identify console browsers, which are not background browsers\n    term_var = \"TERM\"\n    prev_val = None\n    if term_var in os.environ:\n        prev_val = os.environ[term_var]\n        # unsetting it skips console browser discovery\n        os.environ[term_var] = \"\"\n    try:\n        webbrowser.open(url)\n    finally:\n        if prev_val is not None:\n            # restore env variable\n            os.environ[term_var] = prev_val\n"
  },
  {
    "path": "Services/Interfaces/web_interface/util/flask_util.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport flask\n\n\ndef get_rest_reply(json_message, code=200, content_type=\"application/json\"):\n    resp = flask.make_response(json_message, code)\n    resp.headers['Content-Type'] = content_type\n    return resp\n"
  },
  {
    "path": "Services/Interfaces/web_interface/web.py",
    "content": "#  Drakkar-Software OctoBot-Interfaces\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport os\nimport socket\nimport time\nimport flask\nimport flask_cors\nimport flask_socketio\nfrom flask_compress import Compress\nfrom flask_caching import Cache\n\nimport octobot_commons.logging as bot_logging\nimport octobot_services.constants as services_constants\nimport octobot_services.interfaces as services_interfaces\nimport octobot_services.interfaces.util as interfaces_util\nimport octobot_trading.api as trading_api\nimport octobot.configuration_manager as configuration_manager\nimport octobot.enums\nimport tentacles.Services.Interfaces.web_interface.constants as constants\nimport tentacles.Services.Interfaces.web_interface.login as login\nimport tentacles.Services.Interfaces.web_interface.security as security\nimport tentacles.Services.Interfaces.web_interface.websockets as websockets\nimport tentacles.Services.Interfaces.web_interface.plugins as web_interface_plugins\nimport tentacles.Services.Interfaces.web_interface.flask_util as flask_util\nimport tentacles.Services.Interfaces.web_interface.util as web_interface_util\nimport tentacles.Services.Interfaces.web_interface as web_interface_root\nimport tentacles.Services.Interfaces.web_interface.controllers\nimport tentacles.Services.Interfaces.web_interface.advanced_controllers\nimport tentacles.Services.Interfaces.web_interface.api\nimport tentacles.Services.Services_bases as Service_bases\nimport octobot_tentacles_manager.api\n\n\nclass WebInterface(services_interfaces.AbstractWebInterface):\n\n    REQUIRED_SERVICES = [Service_bases.WebService]\n    COLOR_MODE = \"color_mode\"\n    ANNOUNCEMENTS = \"announcements\"\n    DISPLAY_TIME_FRAME = \"display_time_frame\"\n    DISPLAY_ORDERS = \"display_orders\"\n    WATCHED_SYMBOLS = \"watched_symbols\"\n\n    tools = {\n        constants.BOT_TOOLS_BACKTESTING: None,\n        constants.BOT_TOOLS_BACKTESTING_SOURCE: None,\n        constants.BOT_TOOLS_STRATEGY_OPTIMIZER: None,\n        constants.BOT_TOOLS_DATA_COLLECTOR: None,\n        constants.BOT_PREPARING_BACKTESTING: False,\n    }\n\n    def __init__(self, config):\n        super().__init__(config)\n        self.logger = self.get_logger()\n        self.server_instance = None\n        self.host = None\n        self.port = None\n        self.websocket_instance = None\n        self.web_login_manger = None\n        self.requires_password = False\n        self.password_hash = \"\"\n        self.dev_mode = False\n        self.started = False\n        self.registered_plugins = []\n        self._init_web_settings()\n        self.local_config = None\n        if interfaces_util.get_bot_api() is None:\n            # should not happen in non-test environment\n            self.logger.error(\n                f\"interfaces_util.get_bot_api() is not available at {self.get_name()} constructor\"\n            )\n        else:\n            self.reload_config()\n\n    async def register_new_exchange_impl(self, exchange_id):\n        if exchange_id not in self.registered_exchanges_ids:\n            await self._register_on_channels(exchange_id)\n\n    def reload_config(self, tentacles_setup_config=None):\n        self.local_config = octobot_tentacles_manager.api.get_tentacle_config(\n            tentacles_setup_config or interfaces_util.get_edited_tentacles_config(), self.__class__\n        )\n\n    def _init_web_settings(self):\n        try:\n            self.host = os.getenv(services_constants.ENV_WEB_ADDRESS,\n                                  self.config[services_constants.CONFIG_CATEGORY_SERVICES]\n                                  [services_constants.CONFIG_WEB][services_constants.CONFIG_WEB_IP])\n        except KeyError:\n            self.host = os.getenv(services_constants.ENV_WEB_ADDRESS, services_constants.DEFAULT_SERVER_IP)\n        try:\n            self.port = int(os.getenv(services_constants.ENV_WEB_PORT,\n                                      self.config[services_constants.CONFIG_CATEGORY_SERVICES]\n                                      [services_constants.CONFIG_WEB][services_constants.CONFIG_WEB_PORT]))\n        except KeyError:\n            self.port = int(os.getenv(services_constants.ENV_WEB_PORT, services_constants.DEFAULT_SERVER_PORT))\n        try:\n            self.requires_password = \\\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEB] \\\n                    [services_constants.CONFIG_WEB_REQUIRES_PASSWORD]\n        except KeyError:\n            pass\n        try:\n            self.password_hash = self.config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n                [services_constants.CONFIG_WEB][services_constants.CONFIG_WEB_PASSWORD]\n        except KeyError:\n            pass\n        try:\n            env_value = os.getenv(services_constants.ENV_AUTO_OPEN_IN_WEB_BROWSER, None)\n            if env_value is None:\n                self.should_open_web_interface = self.config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n                    [services_constants.CONFIG_WEB][services_constants.CONFIG_AUTO_OPEN_IN_WEB_BROWSER]\n            else:\n                self.should_open_web_interface = env_value.lower() == \"true\"\n        except KeyError:\n            self.should_open_web_interface = True\n        self.dev_mode = False if interfaces_util.get_bot_api() is None else\\\n            interfaces_util.get_edited_config(dict_only=False).dev_mode_enabled()\n\n    @staticmethod\n    async def _web_trades_callback(exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, trade, old_trade):\n        web_interface_root.send_new_trade(\n            trade,\n            exchange_id,\n            symbol\n        )\n\n    @staticmethod\n    async def _web_orders_callback(exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, order,\n                                   update_type, is_from_bot):\n        web_interface_root.send_order_update(order, exchange_id, symbol)\n\n    @staticmethod\n    async def _web_ohlcv_empty_callback(\n            exchange: str,\n            exchange_id: str,\n            cryptocurrency: str,\n            symbol: str,\n            time_frame,\n            candle\n    ):\n        pass\n\n    async def _register_on_channels(self, exchange_id):\n        try:\n            if trading_api.is_exchange_trading(trading_api.get_exchange_manager_from_exchange_id(exchange_id)):\n                await trading_api.subscribe_to_trades_channel(self._web_trades_callback, exchange_id)\n                await trading_api.subscribe_to_order_channel(self._web_orders_callback, exchange_id)\n                await trading_api.subscribe_to_ohlcv_channel(self._web_ohlcv_empty_callback, exchange_id)\n        except ImportError:\n            self.logger.error(\"Watching trade channels requires OctoBot-Trading package installed\")\n\n    def init_flask_plugins_and_config(self, server_instance):\n        # Only setup flask plugins once per flask app (can't call flask setup methods after the 1st request\n        # has been received).\n        # Override system configuration content types\n        flask_util.init_content_types()\n        self.server_instance.json = flask_util.FloatDecimalJSONProvider(self.server_instance)\n\n        # Set CORS policy\n        if flask_util.get_user_defined_cors_allowed_origins() != \"*\":\n            # never allow \"*\" as allowed origin, prefer not setting it if user did not specifically set origins\n            flask_cors.CORS(self.server_instance, origins=flask_util.get_user_defined_cors_allowed_origins())\n\n        self.server_instance.config['SEND_FILE_MAX_AGE_DEFAULT'] = 604800\n\n        if self.dev_mode:\n            server_instance.config['TEMPLATES_AUTO_RELOAD'] = True\n        else:\n            cache = Cache(config={\"CACHE_TYPE\": \"SimpleCache\"})\n            cache.init_app(server_instance)\n\n            Compress(server_instance)\n\n        flask_util.register_context_processor(self)\n        flask_util.register_template_filters(server_instance)\n        # register session secret key\n        server_instance.secret_key = flask_util.BrowsingDataProvider.instance().get_or_create_session_secret_key()\n        self._handle_login(server_instance)\n\n        security.register_responses_extra_header(server_instance, True)\n\n    def _handle_login(self, server_instance):\n        self.web_login_manger = login.WebLoginManager(server_instance, self.password_hash)\n        login.set_is_login_required(self.requires_password)\n\n    def set_requires_password(self, requires_password):\n        self.requires_password = requires_password\n        login.set_is_login_required(requires_password)\n\n    def _register_routes(self, server_instance, distribution: octobot.enums.OctoBotDistribution):\n        tentacles.Services.Interfaces.web_interface.controllers.register(server_instance, distribution)\n        server_instance.register_blueprint(\n            tentacles.Services.Interfaces.web_interface.api.register(distribution)\n        )\n        server_instance.register_blueprint(\n            tentacles.Services.Interfaces.web_interface.advanced_controllers.register(distribution)\n        )\n\n    def _prepare_websocket(self, server_instance):\n        # handles all namespaces without an explicit error handler\n        websocket_instance = flask_socketio.SocketIO(\n            server_instance,\n            async_mode=\"gevent\",\n            cors_allowed_origins=flask_util.get_user_defined_cors_allowed_origins()\n        )\n\n        @websocket_instance.on_error_default\n        def default_error_handler(e):\n            self.logger.exception(e, True, f\"Error with websocket: {e}\")\n\n        for namespace in websockets.namespaces:\n            websocket_instance.on_namespace(namespace)\n\n        bot_logging.register_error_notifier(web_interface_root.send_general_notifications)\n        return websocket_instance\n\n    async def _async_run(self) -> bool:\n        # wait bot is ready\n        while not self.is_bot_ready():\n            time.sleep(0.05)\n\n        try:\n            self.server_instance = flask.Flask(__name__)\n            distribution = configuration_manager.get_distribution(interfaces_util.get_edited_config())\n\n            self._register_routes(self.server_instance, distribution)\n            if distribution is octobot.enums.OctoBotDistribution.DEFAULT:\n                # for now, plugins are only available on default distribution\n                self.registered_plugins = web_interface_plugins.register_all_plugins(\n                    self.server_instance, self.registered_plugins\n                )\n            web_interface_root.update_registered_plugins(self.registered_plugins)\n            self.init_flask_plugins_and_config(self.server_instance)\n            self.websocket_instance = self._prepare_websocket(self.server_instance)\n\n            if self.should_open_web_interface:\n                self._open_web_interface_on_browser()\n\n            self.started = True\n            self.websocket_instance.run(self.server_instance,\n                                        host=self.host,\n                                        port=self.port,\n                                        log_output=False,\n                                        debug=False)\n            return True\n        except Exception as e:\n            self.logger.exception(e, False, f\"Fail to start web interface : {e}\")\n        finally:\n            self.logger.debug(\"Web interface thread stopped\")\n        return False\n\n    def _open_web_interface_on_browser(self):\n        try:\n            web_interface_util.open_in_background_browser(\n                f\"http://{socket.gethostbyname(socket.gethostname())}:{self.port}\"\n            )\n        except Exception as err:\n            self.logger.warning(f\"Impossible to open automatically web interface: {err} ({err.__class__.__name__})\")\n\n    async def _inner_start(self):\n        return self.threaded_start()\n\n    async def stop(self):\n        if self.websocket_instance is not None:\n            try:\n                self.logger.debug(\"Stopping web interface\")\n                self.websocket_instance.stop()\n                self.logger.debug(\"Stopped web interface\")\n            except Exception as e:\n                self.logger.exception(e, False, f\"Error when stopping web interface : {e}\")\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\n\nnamespaces = []\n\n\nfrom tentacles.Services.Interfaces.web_interface.websockets import abstract_websocket_namespace_notifier\nfrom tentacles.Services.Interfaces.web_interface.websockets.abstract_websocket_namespace_notifier import (\n    AbstractWebSocketNamespaceNotifier,\n    websocket_with_login_required_when_activated,\n)\n\nfrom tentacles.Services.Interfaces.web_interface.websockets import data_collector\nfrom tentacles.Services.Interfaces.web_interface.websockets import backtesting\nfrom tentacles.Services.Interfaces.web_interface.websockets import dashboard\nfrom tentacles.Services.Interfaces.web_interface.websockets import notifications\nfrom tentacles.Services.Interfaces.web_interface.websockets import strategy_optimizer\n\n\nfrom tentacles.Services.Interfaces.web_interface.websockets.data_collector import (\n    DataCollectorNamespace,\n)\nfrom tentacles.Services.Interfaces.web_interface.websockets.backtesting import (\n    BacktestingNamespace,\n)\nfrom tentacles.Services.Interfaces.web_interface.websockets.dashboard import (\n    DashboardNamespace,\n)\nfrom tentacles.Services.Interfaces.web_interface.websockets.notifications import (\n    NotificationsNamespace,\n)\nfrom tentacles.Services.Interfaces.web_interface.websockets.strategy_optimizer import (\n    StrategyOptimizerNamespace,\n)\n\n\n__all__ = [\n    \"AbstractWebSocketNamespaceNotifier\",\n    \"websocket_with_login_required_when_activated\",\n    \"BacktestingNamespace\",\n    \"DataCollectorNamespace\",\n    \"DashboardNamespace\",\n    \"NotificationsNamespace\",\n    \"StrategyOptimizerNamespace\",\n]\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/abstract_websocket_namespace_notifier.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport functools\nimport flask_login\nimport flask_socketio\n\nimport octobot_commons.logging as bot_logger\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.login as login\n\n\nclass AbstractWebSocketNamespaceNotifier(flask_socketio.Namespace, web_interface.Notifier):\n\n    def __init__(self, namespace=None):\n        super(flask_socketio.Namespace, self).__init__(namespace)\n        self.logger = bot_logger.get_logger(self.__class__.__name__)\n        # constructor can be called in global project import, in this case manually enable logger\n        self.logger.disable(False)\n        self.clients_count = 0\n\n    def all_clients_send_notifications(self, **kwargs) -> bool:\n        raise NotImplementedError(\"all_clients_send_notifications is not implemented\")\n\n    def on_connect(self):\n        self.clients_count += 1\n\n    def on_disconnect(self, reason):\n        # will be called after some time (requires timeout)\n        self.clients_count -= 1\n\n    def _has_clients(self):\n        return self.clients_count > 0\n\n\ndef websocket_with_login_required_when_activated(func):\n    @functools.wraps(func)\n    def wrapped(self, *args, **kwargs):\n        # Use == because of the flask proxy (this is not a simple python None value)\n        if login.is_login_required() and \\\n                (flask_login.current_user is None or not flask_login.current_user.is_authenticated):\n            flask_socketio.disconnect(self)\n        else:\n            return func(self, *args, **kwargs)\n    return wrapped\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/backtesting.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport flask_socketio\n\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.websockets as websockets\n\n\nclass BacktestingNamespace(websockets.AbstractWebSocketNamespaceNotifier):\n\n    @staticmethod\n    def _get_backtesting_status():\n        backtesting_status, progress, errors = models.get_backtesting_status()\n        return {\"status\": backtesting_status, \"progress\": progress, \"errors\": errors}\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_backtesting_status(self):\n        flask_socketio.emit(\"backtesting_status\", self._get_backtesting_status())\n\n    def all_clients_send_notifications(self, **kwargs) -> bool:\n        if self._has_clients():\n            try:\n                self.socketio.emit(\"backtesting_status\", self._get_backtesting_status(), namespace=self.namespace)\n                return True\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when sending backtesting_status: {e}\")\n        return False\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_connect(self):\n        super().on_connect()\n        self.on_backtesting_status()\n\n\nnotifier = BacktestingNamespace('/backtesting')\nweb_interface.register_notifier(web_interface.BACKTESTING_NOTIFICATION_KEY, notifier)\nwebsockets.namespaces.append(notifier)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/dashboard.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport flask_socketio\n\nimport octobot_commons.pretty_printer as pretty_printer\nimport octobot_trading.enums as trading_enums\nimport octobot_services.interfaces as services_interfaces\nimport octobot_trading.api as octobot_trading_api\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.websockets as websockets\n\n\nclass DashboardNamespace(websockets.AbstractWebSocketNamespaceNotifier):\n\n    @staticmethod\n    def _get_profitability():\n        profitability_digits = None\n        has_real_trader, has_simulated_trader, \\\n        real_global_profitability, simulated_global_profitability, \\\n        real_percent_profitability, simulated_percent_profitability, \\\n        real_no_trade_profitability, simulated_no_trade_profitability, \\\n        market_average_profitability = services_interfaces.get_global_profitability()\n        profitability_data = {\n            \"market_average_profitability\": pretty_printer.round_with_decimal_count(market_average_profitability,\n                                                                                    profitability_digits)\n        }\n        if has_real_trader:\n            profitability_data[\"bot_real_profitability\"] = \\\n                pretty_printer.round_with_decimal_count(real_percent_profitability, profitability_digits)\n            profitability_data[\"bot_real_flat_profitability\"] = \\\n                pretty_printer.round_with_decimal_count(real_global_profitability, profitability_digits)\n            profitability_data[\"real_no_trade_profitability\"] = \\\n                pretty_printer.round_with_decimal_count(real_no_trade_profitability, profitability_digits)\n        if has_simulated_trader:\n            profitability_data[\"bot_simulated_profitability\"] = \\\n                pretty_printer.round_with_decimal_count(simulated_percent_profitability, profitability_digits)\n            profitability_data[\"bot_simulated_flat_profitability\"] = \\\n                pretty_printer.round_with_decimal_count(simulated_global_profitability, profitability_digits)\n            profitability_data[\"simulated_no_trade_profitability\"] = \\\n                pretty_printer.round_with_decimal_count(simulated_no_trade_profitability, profitability_digits)\n        return profitability_data\n\n    @staticmethod\n    def _format_new_data(exchange_id=None, trades=None, order=None, symbol=None):\n        exchange_manager = octobot_trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n        return {\n            \"trades\": models.format_trades(trades),\n            \"orders\": models.format_orders(octobot_trading_api.get_open_orders(exchange_manager, symbol=symbol), 0),\n            \"simulated\": octobot_trading_api.is_trader_simulated(exchange_manager),\n            \"symbol\": symbol,\n            \"exchange_id\": exchange_id\n        }\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_profitability(self):\n        flask_socketio.emit(\"profitability\", self._get_profitability())\n\n    def all_clients_send_notifications(self, **kwargs) -> bool:\n        if self._has_clients():\n            try:\n                self.socketio.emit(\"new_data\",\n                                   {\n                                       \"data\": self._format_new_data(**kwargs)\n                                   },\n                                   namespace=self.namespace)\n                return True\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when sending web notification: {e}\")\n        return False\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_candle_graph_update(self, data):\n        try:\n            flask_socketio.emit(\"candle_graph_update_data\", {\n                \"request\": data,\n                \"data\": models.get_currency_price_graph_update(data[\"exchange_id\"],\n                                                               models.get_value_from_dict_or_string(data[\"symbol\"]),\n                                                               data[\"time_frame\"],\n                                                               backtesting=False,\n                                                               minimal_candles=True,\n                                                               ignore_trades=True,\n                                                               ignore_orders=not models.get_display_orders())\n            })\n        except KeyError:\n            flask_socketio.emit(\"error\", \"missing exchange manager\")\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_connect(self):\n        super().on_connect()\n        self.on_profitability()\n\n\nnotifier = DashboardNamespace('/dashboard')\nweb_interface.register_notifier(web_interface.DASHBOARD_NOTIFICATION_KEY, notifier)\nwebsockets.namespaces.append(notifier)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/data_collector.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport flask_socketio\n\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.websockets as websockets\n\n\nclass DataCollectorNamespace(websockets.AbstractWebSocketNamespaceNotifier):\n\n    @staticmethod\n    def _get_data_collector_status():\n        data_collector_status, progress = models.get_data_collector_status()\n        return {\"status\": data_collector_status, \"progress\": progress}\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_data_collector_status(self):\n        flask_socketio.emit(\"data_collector_status\", self._get_data_collector_status())\n\n    def all_clients_send_notifications(self, **kwargs) -> bool:\n        if self._has_clients():\n            try:\n                self.socketio.emit(\"data_collector_status\", self._get_data_collector_status(), namespace=self.namespace)\n                return True\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when sending backtesting_status: {e}\")\n        return False\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_connect(self):\n        super().on_connect()\n        self.on_data_collector_status()\n\n\nnotifier = DataCollectorNamespace('/data_collector')\nweb_interface.register_notifier(web_interface.DATA_COLLECTOR_NOTIFICATION_KEY, notifier)\nwebsockets.namespaces.append(notifier)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/notifications.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport copy\nimport flask_socketio\n\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.websockets as websockets\n\n\nclass NotificationsNamespace(websockets.AbstractWebSocketNamespaceNotifier):\n\n    @staticmethod\n    def _get_update_data():\n        return {\n            \"notifications\": web_interface.get_notifications(),\n            \"errors_count\": web_interface.get_errors_count()\n        }\n\n    def _client_context_send_notifications(self):\n        flask_socketio.emit(\"update\", self._get_update_data())\n\n    def all_clients_send_notifications(self, **kwargs) -> bool:\n        if self._has_clients():\n            try:\n                self.socketio.emit(\"update\", self._get_update_data(), namespace=self.namespace)\n                return True\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when sending web notification: {e}\")\n        return False\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_connect(self):\n        super().on_connect()\n        self._client_context_send_notifications()\n        web_interface.flush_notifications()\n\n\nnotifier = NotificationsNamespace('/notifications')\nweb_interface.register_notifier(web_interface.GENERAL_NOTIFICATION_KEY, notifier)\nwebsockets.namespaces.append(notifier)\n"
  },
  {
    "path": "Services/Interfaces/web_interface/websockets/strategy_optimizer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport flask_socketio\n\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Interfaces.web_interface.models as models\nimport tentacles.Services.Interfaces.web_interface.websockets as websockets\n\n\nclass StrategyOptimizerNamespace(websockets.AbstractWebSocketNamespaceNotifier):\n\n    @staticmethod\n    def _get_strategy_optimizer_status():\n        optimizer_status, progress, overall_progress, remaining_time, errors = models.get_optimizer_status()\n        return {\n            \"status\": optimizer_status,\n            \"progress\": progress,\n            \"overall_progress\": overall_progress,\n            \"remaining_time\": remaining_time,\n            \"errors\": errors\n        }\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_strategy_optimizer_status(self):\n        flask_socketio.emit(\"strategy_optimizer_status\", self._get_strategy_optimizer_status())\n\n    def all_clients_send_notifications(self, **kwargs) -> bool:\n        if self._has_clients():\n            try:\n                self.socketio.emit(\"strategy_optimizer_status\", self._get_strategy_optimizer_status(),\n                                   namespace=self.namespace)\n                return True\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when sending strategy_optimizer_status: {e}\")\n        return False\n\n    @websockets.websocket_with_login_required_when_activated\n    def on_connect(self):\n        super().on_connect()\n        self.on_strategy_optimizer_status()\n\n\nnotifier = StrategyOptimizerNamespace('/strategy_optimizer')\nweb_interface.register_notifier(web_interface.STRATEGY_OPTIMIZER_NOTIFICATION_KEY, notifier)\nwebsockets.namespaces.append(notifier)\n"
  },
  {
    "path": "Services/Notifiers/telegram_notifier/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .telegram import TelegramNotifier"
  },
  {
    "path": "Services/Notifiers/telegram_notifier/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramNotifier\"],\n  \"tentacles-requirements\": [\"telegram_service\"]\n}"
  },
  {
    "path": "Services/Notifiers/telegram_notifier/telegram.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.enums as commons_enums\nimport octobot_services.notification as notification\nimport octobot_services.notifier as notifier\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass TelegramNotifier(notifier.AbstractNotifier):\n    REQUIRED_SERVICES = [Services_bases.TelegramService]\n    NOTIFICATION_TYPE_KEY = \"telegram\"\n    USE_MAIN_LOOP = True\n\n    async def _handle_notification(self, notification: notification.Notification):\n        self.logger.debug(f\"sending notification: {notification}\")\n        text, use_markdown = self._get_message_text(notification)\n        await self._send_message(notification, text, use_markdown)\n\n    async def _send_message(self, notification, text, use_markdown):\n        try:\n            previous_message_id = notification.linked_notification.metadata[self.NOTIFICATION_TYPE_KEY].message_id \\\n                if notification.linked_notification and \\\n                   self.NOTIFICATION_TYPE_KEY in notification.linked_notification.metadata else None\n        except (KeyError, AttributeError):\n            previous_message_id = None\n        sent_message = await self.services[0].send_message(text,\n                                                           markdown=use_markdown,\n                                                           reply_to_message_id=previous_message_id)\n        if sent_message is None and previous_message_id is not None:\n            # failed to reply, try regular message\n            self.logger.warning(f\"Failed to reply to message with id {previous_message_id}, sending regular message.\")\n            sent_message = await self.services[0].send_message(text,\n                                                               markdown=use_markdown,\n                                                               reply_to_message_id=None)\n        notification.metadata[self.NOTIFICATION_TYPE_KEY] = sent_message\n\n    @staticmethod\n    def _get_message_text(notification):\n        title = notification.title\n        text = notification.markdown_text if notification.markdown_text else notification.text\n        if notification.markdown_format not in (commons_enums.MarkdownFormat.NONE, commons_enums.MarkdownFormat.IGNORE):\n            text = f\"{notification.markdown_format.value}{text}{notification.markdown_format.value}\"\n        if title:\n            title = f\"{commons_enums.MarkdownFormat.CODE.value}{title}{commons_enums.MarkdownFormat.CODE.value}\"\n            text = f\"{title}\\n{text}\"\n        use_markdown = notification.markdown_format is not commons_enums.MarkdownFormat.NONE\n        return text, use_markdown\n"
  },
  {
    "path": "Services/Notifiers/twitter_notifier/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .twitter import TwitterNotifier"
  },
  {
    "path": "Services/Notifiers/twitter_notifier/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TwitterNotifier\"],\n  \"tentacles-requirements\": [\"twitter_service\"]\n}"
  },
  {
    "path": "Services/Notifiers/twitter_notifier/twitter.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_services.notification as notification\nimport octobot_services.notifier as notifier\nimport tentacles.Services.Services_bases as Services_bases\n\n\n# disable inheritance to disable tentacle visibility. Disabled as starting from feb 9 2023, API is now paid only\n# class TwitterNotifier(notifier.AbstractNotifier):\nclass TwitterNotifier:\n    REQUIRED_SERVICES = [Services_bases.TwitterService]\n    NOTIFICATION_TYPE_KEY = \"twitter\"\n\n    async def _handle_notification(self, notification: notification.Notification):\n        self.logger.debug(f\"sending notification: {notification}\")\n        if notification.linked_notification is None:\n            result = await self._send_regular_tweet(notification)\n        else:\n            result = await self._send_tweet_reply(notification)\n        if result is None:\n            self.logger.error(f\"Tweet is not sent, notification: {notification}\")\n        else:\n            self.logger.info(\"Tweet sent\")\n\n    async def _send_regular_tweet(self, notification):\n        result = await self.services[0].post(self._get_tweet_text(notification), True)\n        notification.metadata[self.NOTIFICATION_TYPE_KEY] = result\n        return result\n\n    async def _send_tweet_reply(self, notification):\n        try:\n            previous_tweet_id = notification.linked_notification.metadata[self.NOTIFICATION_TYPE_KEY].id\n            result = await self.services[0].respond(previous_tweet_id, self._get_tweet_text(notification), True)\n            notification.metadata[self.NOTIFICATION_TYPE_KEY] = result\n            return result\n        except (KeyError, AttributeError):\n            return await self._send_regular_tweet(notification)\n\n    @staticmethod\n    def _get_tweet_text(notification):\n        return f\"{notification.title}\\n{notification.text}\" if notification.title else notification.text\n"
  },
  {
    "path": "Services/Notifiers/web_notifier/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .web import WebNotifier"
  },
  {
    "path": "Services/Notifiers/web_notifier/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"WebNotifier\"],\n  \"tentacles-requirements\": [\"web_service\"]\n}"
  },
  {
    "path": "Services/Notifiers/web_notifier/web.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_services.notification as services_notification\nimport octobot_services.notifier as notifier\nimport tentacles.Services.Interfaces.web_interface as web_interface\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass WebNotifier(notifier.AbstractNotifier):\n    REQUIRED_SERVICES = [Services_bases.WebService]\n    NOTIFICATION_TYPE_KEY = \"web\"\n\n    async def _handle_notification(self, notification: services_notification.Notification):\n        await web_interface.add_notification(notification.level, notification.title,\n                                             notification.text.replace(\"\\n\", \"<br>\"),\n                                             sound=notification.sound.value)\n"
  },
  {
    "path": "Services/Services_bases/google_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .google import GoogleService"
  },
  {
    "path": "Services/Services_bases/google_service/google.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_services.constants as services_constants\nimport octobot_services.services as services\n\n\nclass GoogleService(services.AbstractService):\n    @staticmethod\n    def is_setup_correctly(config):\n        return True\n\n    @staticmethod\n    def get_is_enabled(config):\n        return True\n\n    def has_required_configuration(self):\n        return True\n\n    def get_endpoint(self) -> None:\n        return None\n\n    def get_type(self) -> None:\n        return services_constants.CONFIG_GOOGLE\n\n    async def prepare(self) -> None:\n        pass\n\n    def get_successful_startup_message(self):\n        return \"\", True\n"
  },
  {
    "path": "Services/Services_bases/google_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GoogleService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/gpt_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .gpt import GPTService"
  },
  {
    "path": "Services/Services_bases/gpt_service/gpt.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport os\nimport typing\nimport uuid\nimport openai\nimport logging\nimport datetime\n\nimport octobot_services.constants as services_constants\nimport octobot_services.services as services\nimport octobot_services.errors as errors\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.logging as commons_logging\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_commons.authentication as authentication\nimport octobot_commons.tree as tree\nimport octobot_commons.configuration.fields_utils as fields_utils\n\nimport octobot.constants as constants\nimport octobot.community as community\n\n\nNO_SYSTEM_PROMPT_MODELS = [\n    \"o1-mini\",\n]\nMINIMAL_PARAMS_SERIES_MODELS = [\n    \"o\", # the whole o-series does not support temperature parameter\n]\nMINIMAL_PARAMS_MODELS = [\n    \"gpt-5\", # does not support temperature parameter\n]\nSYSTEM = \"system\"\nUSER = \"user\"\n\n\nclass GPTService(services.AbstractService):\n    BACKTESTING_ENABLED = True\n    DEFAULT_MODEL = \"gpt-3.5-turbo\"\n    NO_TOKEN_LIMIT_VALUE = -1\n\n    def get_fields_description(self):\n        if self._env_secret_key is None:\n            return {\n                services_constants.CONIG_OPENAI_SECRET_KEY: \"Your openai API secret key\",\n                services_constants.CONIG_LLM_CUSTOM_BASE_URL: (\n                    \"Custom LLM base url to use. Leave empty to use openai.com. For Ollama models, \"\n                    \"add /v1 to the url (such as: http://localhost:11434/v1)\"\n                ),\n            }\n        return {}\n\n    def get_default_value(self):\n        if self._env_secret_key is None:\n            return {\n                services_constants.CONIG_OPENAI_SECRET_KEY: \"\",\n                services_constants.CONIG_LLM_CUSTOM_BASE_URL: \"\",\n            }\n        return {}\n\n    def __init__(self):\n        super().__init__()\n        logging.getLogger(\"openai\").setLevel(logging.WARNING)\n        self._env_secret_key: str = os.getenv(services_constants.ENV_OPENAI_SECRET_KEY, None) or None\n        self.model: str = os.getenv(services_constants.ENV_GPT_MODEL, self.DEFAULT_MODEL)\n        self.stored_signals: tree.BaseTree = tree.BaseTree()\n        self.models: list[str] = []\n        self._env_daily_token_limit: int = int(os.getenv(\n            services_constants.ENV_GPT_DAILY_TOKENS_LIMIT,\n            self.NO_TOKEN_LIMIT_VALUE)\n        )\n        self._daily_tokens_limit: int = self._env_daily_token_limit\n        self.consumed_daily_tokens: int = 1\n        self.last_consumed_token_date: datetime.date = None\n\n    @staticmethod\n    def create_message(role, content, model: str = None):\n        if role == SYSTEM and model in NO_SYSTEM_PROMPT_MODELS:\n            commons_logging.get_logger(GPTService.__name__).debug(\n                f\"Overriding prompt to use {USER} instead of {SYSTEM} for {model}\"\n            )\n            return {\"role\": USER, \"content\": content}\n        return {\"role\": role, \"content\": content}\n\n    async def get_chat_completion(\n        self,\n        messages,\n        model=None,\n        max_tokens=3000,\n        n=1,\n        stop=None,\n        temperature=0.5,\n        exchange: str = None,\n        symbol: str = None,\n        time_frame: str = None,\n        version: str = None,\n        candle_open_time: float = None,\n        use_stored_signals: bool = False,\n    ) -> str:\n        if use_stored_signals:\n            return self._get_signal_from_stored_signals(exchange, symbol, time_frame, version, candle_open_time)\n        if self.use_stored_signals_only():\n            signal = await self._fetch_signal_from_stored_signals(exchange, symbol, time_frame, version, candle_open_time)\n            if not signal:\n                # should not happen\n                self.logger.error(\n                    f\"Missing ChatGPT signal from stored signals on {symbol} {time_frame} \"\n                    f\"for timestamp: {candle_open_time} with version: {version}\"\n                )\n            return signal\n        return await self._get_signal_from_gpt(messages, model, max_tokens, n, stop, temperature)\n\n    def _get_client(self) -> openai.AsyncOpenAI:\n        return openai.AsyncOpenAI(\n            api_key=self._get_api_key(),\n            base_url=self._get_base_url(),\n        )\n\n    def _is_of_series(self, model: str, series: str) -> bool:\n        if model.startswith(series) and len(model) > 1:\n            # avoid false positive: check if the next character is a number (ex: o3 model)\n            try:\n                int(model[len(series)])\n                return True\n            except ValueError:\n                return False\n        return False\n\n    def _is_minimal_params_model(self, model: str) -> bool:\n        for minimal_params_series in MINIMAL_PARAMS_SERIES_MODELS:\n            if self._is_of_series(model, minimal_params_series):\n                return True\n        for minimal_params_model in MINIMAL_PARAMS_MODELS:\n            if model.startswith(minimal_params_model):\n                return True\n        return False\n\n    async def _get_signal_from_gpt(\n        self,\n        messages,\n        model=None,\n        max_tokens=3000,\n        n=1,\n        stop=None,\n        temperature=0.5\n    ):\n        self._ensure_rate_limit()\n        try:\n            model = model or self.model\n            supports_params = not self._is_minimal_params_model(model)\n            if not supports_params:\n                self.logger.info(\n                    f\"The {model} model does not support every required parameter, results might not be as accurate \"\n                    f\"as with other models.\"\n                )\n            completions = await self._get_client().chat.completions.create(\n                model=model,\n                max_completion_tokens=max_tokens,\n                n=n,\n                stop=stop,\n                temperature=temperature if supports_params else openai.NOT_GIVEN,\n                messages=messages\n            )\n            self._update_token_usage(completions.usage.total_tokens)\n            return completions.choices[0].message.content\n        except (\n            openai.BadRequestError, openai.UnprocessableEntityError # error in request\n        )as err:\n            if \"does not support 'system' with this model\" in str(err):\n                desc = err.message\n                err_message = (\n                    f\"The \\\"{model}\\\" model can't be used with {SYSTEM} prompts. \"\n                    f\"It should be added to NO_SYSTEM_PROMPT_MODELS: {desc}\"\n            )\n            else:\n                err_message = f\"Error when running request with model {model} (invalid request): {err}\"\n            raise errors.InvalidRequestError(err_message) from err\n        except openai.NotFoundError as err:\n            self.logger.error(f\"Model {model} not found: {err}. Available models: {', '.join(self.models)}\")\n            self.creation_error_message = str(err)\n        except openai.AuthenticationError as err:\n            self.logger.error(f\"Invalid OpenAI api key: {err}\")\n            self.creation_error_message = str(err)\n        except Exception as err:\n            raise errors.InvalidRequestError(\n                f\"Unexpected error when running request with model {model}: {err}\"\n            ) from err\n\n    def _get_signal_from_stored_signals(\n        self,\n        exchange: str,\n        symbol: str,\n        time_frame: str,\n        version: str,\n        candle_open_time: float,\n    ) -> str:\n        try:\n            return self.stored_signals.get_node([exchange, symbol, time_frame, version, candle_open_time]).node_value\n        except tree.NodeExistsError:\n            return \"\"\n\n    async def _fetch_signal_from_stored_signals(\n        self,\n        exchange: str,\n        symbol: str,\n        time_frame: str,\n        version: str,\n        candle_open_time: float,\n    ) -> typing.Optional[str]:\n        authenticator = authentication.Authenticator.instance()\n        try:\n            return await authenticator.get_gpt_signal(\n                exchange, symbol, commons_enums.TimeFrames(time_frame), candle_open_time, version\n            )\n        except Exception as err:\n            self.logger.exception(err, True, f\"Error when fetching gpt signal: {err}\")\n\n    def store_signal_history(\n        self,\n        exchange: str,\n        symbol: str,\n        time_frame: commons_enums.TimeFrames,\n        version: str,\n        signals_by_candle_open_time,\n    ):\n        tf = time_frame.value\n        for candle_open_time, signal in signals_by_candle_open_time.items():\n            self.stored_signals.set_node_at_path(\n                signal,\n                str,\n                [exchange, symbol, tf, version, candle_open_time]\n            )\n\n    def has_signal_history(\n        self,\n        exchange: str,\n        symbol: str,\n        time_frame: commons_enums.TimeFrames,\n        min_timestamp: float,\n        max_timestamp: float,\n        version: str\n    ):\n        for ts in (min_timestamp, max_timestamp):\n            if self._get_signal_from_stored_signals(\n                exchange, symbol, time_frame.value, version, time_frame_manager.get_last_timeframe_time(time_frame, ts)\n            ) == \"\":\n                return False\n        return True\n\n    async def _fetch_and_store_history(\n        self, authenticator, exchange_name, symbol, time_frame, version, min_timestamp: float, max_timestamp: float\n    ):\n        # no need to fetch a particular exchange\n        signals_by_candle_open_time = await authenticator.get_gpt_signals_history(\n            None, symbol, time_frame,\n            time_frame_manager.get_last_timeframe_time(time_frame, min_timestamp),\n            time_frame_manager.get_last_timeframe_time(time_frame, max_timestamp),\n            version\n        )\n        if signals_by_candle_open_time:\n            self.logger.info(\n                f\"Fetched {len(signals_by_candle_open_time)} ChatGPT signals \"\n                f\"history for {symbol} {time_frame} on any exchange.\"\n            )\n        else:\n            self.logger.error(\n                f\"No ChatGPT signal history for {symbol} on {time_frame.value} for any exchange with {version}. \"\n                f\"Please check {self._supported_history_url()} to get the list of supported signals history.\"\n            )\n        self.store_signal_history(\n            exchange_name, symbol, time_frame, version, signals_by_candle_open_time\n        )\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return True\n\n    async def fetch_gpt_history(\n        self, exchange_name: str, symbols: list, time_frames: list,\n        version: str, start_timestamp: float, end_timestamp: float\n    ):\n        authenticator = authentication.Authenticator.instance()\n        coros = [\n            self._fetch_and_store_history(\n                authenticator, exchange_name, symbol, time_frame, version, start_timestamp, end_timestamp\n            )\n            for symbol in symbols\n            for time_frame in time_frames\n            if not self.has_signal_history(exchange_name, symbol, time_frame, start_timestamp, end_timestamp, version)\n        ]\n        if coros:\n            await asyncio.gather(*coros)\n\n    def clear_signal_history(self):\n        self.stored_signals.clear()\n\n    def allow_token_limit_update(self):\n        return self._env_daily_token_limit == self.NO_TOKEN_LIMIT_VALUE\n\n    def apply_daily_token_limit_if_possible(self, updated_limit: int):\n        # do not allow updating daily_tokens_limit when set from environment variables\n        if self.allow_token_limit_update():\n            self._daily_tokens_limit = updated_limit\n\n    def _supported_history_url(self):\n        return f\"{community.IdentifiersProvider.COMMUNITY_URL}/features/chatgpt-trading\"\n\n    def _ensure_rate_limit(self):\n        if self.last_consumed_token_date != datetime.date.today():\n            self.consumed_daily_tokens = 0\n            self.last_consumed_token_date = datetime.date.today()\n        if self._daily_tokens_limit == self.NO_TOKEN_LIMIT_VALUE:\n            return\n        if self.consumed_daily_tokens >= self._daily_tokens_limit:\n            raise errors.RateLimitError(\n                f\"Daily rate limit reached (used {self.consumed_daily_tokens} out of {self._daily_tokens_limit})\"\n            )\n\n    def _update_token_usage(self, consumed_tokens):\n        self.consumed_daily_tokens += consumed_tokens\n        self.logger.debug(f\"Consumed {consumed_tokens} tokens. {self.consumed_daily_tokens} consumed tokens today.\")\n\n    def check_required_config(self, config):\n        if self._env_secret_key is not None or self.use_stored_signals_only() or self._get_base_url():\n            return True\n        try:\n            config_key = config[services_constants.CONIG_OPENAI_SECRET_KEY]\n            return bool(config_key) and config_key not in commons_constants.DEFAULT_CONFIG_VALUES\n        except KeyError:\n            return False\n\n    def has_required_configuration(self):\n        try:\n            if self.use_stored_signals_only():\n                return True\n            return self.check_required_config(\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES].get(services_constants.CONFIG_GPT, {})\n            )\n        except KeyError:\n            return False\n\n    def get_required_config(self):\n        return [] if self._env_secret_key else [services_constants.CONIG_OPENAI_SECRET_KEY]\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/chatgpt\"\n\n    def get_type(self) -> str:\n        return services_constants.CONFIG_GPT\n\n    def get_website_url(self):\n        return \"https://platform.openai.com/overview\"\n\n    def get_logo(self):\n        return \"https://upload.wikimedia.org/wikipedia/commons/0/04/ChatGPT_logo.svg\"\n    \n    def _get_api_key(self):\n        key = (\n            self._env_secret_key or\n            self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT].get(\n                services_constants.CONIG_OPENAI_SECRET_KEY, None\n            )\n        )\n        if key and not fields_utils.has_invalid_default_config_value(key):\n            return key\n        if self._get_base_url():\n            # no key and custom base url: use random key\n            return uuid.uuid4().hex\n        return key\n\n    def _get_base_url(self):\n        value = self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT].get(\n            services_constants.CONIG_LLM_CUSTOM_BASE_URL\n        )\n        if fields_utils.has_invalid_default_config_value(value):\n            return None\n        return value or None\n\n    async def prepare(self) -> None:\n        try:\n            if self.use_stored_signals_only():\n                self.logger.info(f\"Skipping GPT - OpenAI models fetch as self.use_stored_signals_only() is True\")\n                return\n            if self._get_base_url():\n                self.logger.info(f\"Using custom LLM url: {self._get_base_url()}\")\n            fetched_models = await self._get_client().models.list()\n            if fetched_models.data:\n                self.logger.info(f\"Fetched {len(fetched_models.data)} models\")\n                self.models = [d.id for d in fetched_models.data]\n            else:\n                self.logger.info(\"No fetched models\")\n                self.models = []\n            if self.model not in self.models:\n                if self._get_base_url():\n                    self.logger.info(\n                        f\"Custom LLM available models are: {self.models}. \"\n                        f\"Please select one of those in your evaluator configuration.\"\n                    )\n                else:\n                    self.logger.warning(\n                        f\"Warning: the default '{self.model}' model is not in available LLM models from the \"\n                        f\"selected LLM provider. \"\n                        f\"Available models are: {self.models}. Please select an available model when configuring your \"\n                        f\"evaluators.\"\n                    )\n        except openai.AuthenticationError as err:\n            self.logger.error(f\"Invalid OpenAI api key: {err}\")\n            self.creation_error_message = str(err)\n        except Exception as err:\n            self.logger.exception(err, True, f\"Unexpected error when initializing GPT service: {err}\")\n\n    def _is_healthy(self):\n        return self.use_stored_signals_only() or (self._get_api_key() and self.models)\n\n    def get_successful_startup_message(self):\n        return f\"GPT configured and ready. {len(self.models)} AI models are available. \" \\\n               f\"Using {'stored signals' if self.use_stored_signals_only() else self.models}.\", \\\n            self._is_healthy()\n\n    def use_stored_signals_only(self):\n        return not self.config\n\n    async def stop(self):\n        pass\n"
  },
  {
    "path": "Services/Services_bases/gpt_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GPTService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/reddit_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .reddit import RedditService\n"
  },
  {
    "path": "Services/Services_bases/reddit_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"RedditService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/reddit_service/reddit.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport asyncpraw\n\nimport octobot_services.constants as services_constants\nimport octobot_services.services as services\nimport octobot.constants as constants\n\n\nclass RedditService(services.AbstractService):\n    CLIENT_ID = \"client-id\"\n    CLIENT_SECRET = \"client-secret\"\n\n    def __init__(self):\n        super().__init__()\n        self.reddit_api = None\n\n    def get_fields_description(self):\n        return {\n            self.CLIENT_ID: \"Your client ID.\",\n            self.CLIENT_SECRET: \"Your client ID secret.\",\n        }\n\n    def get_default_value(self):\n        return {\n            self.CLIENT_ID: \"\",\n            self.CLIENT_SECRET: \"\"\n        }\n\n    def get_required_config(self):\n        return [self.CLIENT_ID, self.CLIENT_SECRET]\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/reddit\"\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return services_constants.CONFIG_REDDIT in config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and services_constants.CONFIG_SERVICE_INSTANCE in config[services_constants.CONFIG_CATEGORY_SERVICES][\n                   services_constants.CONFIG_REDDIT]\n\n    def create_reddit_api(self):\n        self.reddit_api = \\\n            asyncpraw.Reddit(client_id=\n                             self.config[services_constants.CONFIG_CATEGORY_SERVICES][\n                                 services_constants.CONFIG_REDDIT][\n                                 self.CLIENT_ID],\n                             client_secret=\n                             self.config[services_constants.CONFIG_CATEGORY_SERVICES][\n                                 services_constants.CONFIG_REDDIT][\n                                 self.CLIENT_SECRET],\n                             user_agent='bot',\n                             **self.mocked_asyncpraw_ini()\n                             )\n\n    async def prepare(self):\n        if not self.reddit_api:\n            try:\n                self.create_reddit_api()\n            except KeyError:\n                asyncpraw.createIni()\n\n    def get_type(self):\n        return services_constants.CONFIG_REDDIT\n\n    def get_website_url(self):\n        return \"https://www.reddit.com\"\n\n    def get_endpoint(self):\n        return self.reddit_api\n\n    def has_required_configuration(self):\n        return services_constants.CONFIG_CATEGORY_SERVICES in self.config \\\n               and services_constants.CONFIG_REDDIT in self.config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and self.check_required_config(\n            self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_REDDIT])\n\n    def get_successful_startup_message(self):\n        return f\"Successfully initialized.\", True\n\n    def mocked_asyncpraw_ini(self):\n        # asyncpraw praw.ini file is sometimes not found in binary env, mock its values.\n        # mock values from https://github.com/praw-dev/praw/blob/master/praw/praw.ini using [DEFAULT]\n        # warning, on updating the asycpraw lib, make sure this file did not change\n        # last update: 24 aug 2022 with asyncpraw==7.5.0\n        # file:\n        # [DEFAULT]\n        # # A boolean to indicate whether or not to check for package updates.\n        # check_for_updates = True\n        #\n        # # Object to kind mappings\n        # comment_kind = t1\n        # message_kind = t4\n        # redditor_kind = t2\n        # submission_kind = t3\n        # subreddit_kind = t5\n        # trophy_kind = t6\n        #\n        # # The URL prefix for OAuth-related requests.\n        # oauth_url = https: // oauth.reddit.com\n        #\n        # # The amount of seconds of ratelimit to sleep for upon encountering a specific type of 429 error.\n        # ratelimit_seconds = 5\n        #\n        # # The URL prefix for regular requests.\n        # reddit_url = https: // www.reddit.com\n        #\n        # # The URL prefix for short URLs.\n        # short_url = https: // redd.it\n        #\n        # # The timeout for requests to Reddit in number of seconds\n        # timeout = 16\n        return {\n            \"check_for_updates\": \"False\",  # local overwrite to avoid update check at startup\n\n            \"comment_kind\": \"t1\",\n            \"message_kind\": \"t4\",\n            \"redditor_kind\": \"t2\",\n            \"submission_kind\": \"t3\",\n            \"subreddit_kind\": \"t5\",\n            \"trophy_kind\": \"t6\",\n\n            \"oauth_url\": \"https://oauth.reddit.com\",\n\n            \"ratelimit_seconds\": \"5\",\n\n            \"reddit_url\": \"https://www.reddit.com\",\n\n            \"short_url\": \"https://redd.it\",\n\n            \"timeout\": \"16\",\n        }\n"
  },
  {
    "path": "Services/Services_bases/telegram_api_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .telegram_api import TelegramApiService"
  },
  {
    "path": "Services/Services_bases/telegram_api_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramApiService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/telegram_api_service/telegram_api.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport logging\nimport os\n\nimport octobot_commons.constants as common_constants\nimport octobot_commons.logging as bot_logging\nimport telegram\nimport telethon\n\nimport octobot.constants as constants\nimport octobot_services.constants as services_constants\nimport octobot_services.enums as services_enums\nimport octobot_services.services as services\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n\nclass TelegramApiService(services.AbstractService):\n    LOGGERS = [\"TelegramApiService.client.updates\", \"TelegramApiService.extensions.messagepacker\",\n               \"TelegramApiService.network.mtprotosender\", \"TelegramApiService.client.downloads\",\n               \"telethon.crypto.aes\", \"telethon.crypto.aesctr\"]\n\n    DOWNLOADS_FOLDER = \"Downloads\"\n\n    def __init__(self):\n        super().__init__()\n        self.telegram_client: telethon.TelegramClient = None\n        self.user_account = None\n        self.connected = False\n        self.tentacle_resources_path = tentacles_manager_api.get_tentacle_resources_path(self.__class__)\n        bot_logging.set_logging_level(self.LOGGERS, logging.WARNING)\n\n    def get_fields_description(self):\n        return {\n            services_constants.CONFIG_API: \"App api key.\",\n            services_constants.CONFIG_API_HASH: \"App api hash.\",\n            services_constants.CONFIG_TELEGRAM_PHONE: \"Your telegram phone number (beginning with '+' country code).\",\n        }\n\n    def get_default_value(self):\n        return {\n            services_constants.CONFIG_API: \"\",\n            services_constants.CONFIG_API_HASH: \"\",\n            services_constants.CONFIG_TELEGRAM_PHONE: \"\"\n        }\n\n    def add_event_handler(self, callback, event):\n        if self.telegram_client:\n            self.telegram_client.add_event_handler(callback, event)\n\n    def get_required_config(self):\n        return [services_constants.CONFIG_API, services_constants.CONFIG_API_HASH]\n\n    def get_read_only_info(self) -> list[services.ReadOnlyInfo]:\n        return [\n            services.ReadOnlyInfo(\n                f\"Connected as {self.user_account.username}\",\n                f\"https://telegram.me/{self.user_account.username}\",\n                services_enums.ReadOnlyInfoType.COPYABLE\n            )\n        ] if self.connected and self.user_account else []\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/telegram/telegram-api\"\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return services_constants.CONFIG_TELEGRAM_API in config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and services_constants.CONFIG_SERVICE_INSTANCE in config[services_constants.CONFIG_CATEGORY_SERVICES][\n                   services_constants.CONFIG_TELEGRAM_API]\n\n    async def prepare(self):\n        if not self.telegram_client:\n            try:\n                self.telegram_client = telethon.TelegramClient(f\"{common_constants.USER_FOLDER}/telegram-api\",\n                                                               self.config[services_constants.CONFIG_CATEGORY_SERVICES]\n                                                               [services_constants.CONFIG_TELEGRAM_API]\n                                                               [services_constants.CONFIG_API],\n                                                               self.config[services_constants.CONFIG_CATEGORY_SERVICES]\n                                                               [services_constants.CONFIG_TELEGRAM_API]\n                                                               [services_constants.CONFIG_API_HASH],\n                                                               base_logger=self.get_name())\n\n                await self.telegram_client.start(\n                    phone=\n                    self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TELEGRAM_API]\n                    [services_constants.CONFIG_TELEGRAM_PHONE]\n                )\n                self.user_account = await self.telegram_client.get_me()\n                self.connected = True\n            except Exception as e:\n                self.logger.error(f\"Failed to connect to Telegram Api : {e}\")\n\n    def is_running(self):\n        return self.telegram_client.is_connected()\n\n    def get_type(self):\n        return services_constants.CONFIG_TELEGRAM_API\n\n    def get_website_url(self):\n        return \"https://telegram.org/\"\n\n    def get_endpoint(self):\n        return self.telegram_client\n\n    def get_brand_name(self):\n        return \"telegram\"\n\n    async def stop(self):\n        if self.connected:\n            self.telegram_client.disconnect()\n            self.connected = False\n\n    @staticmethod\n    def get_is_enabled(config):\n        return services_constants.CONFIG_CATEGORY_SERVICES in config \\\n               and services_constants.CONFIG_TELEGRAM_API in config[services_constants.CONFIG_CATEGORY_SERVICES]\n\n    def has_required_configuration(self):\n        return services_constants.CONFIG_CATEGORY_SERVICES in self.config \\\n               and services_constants.CONFIG_TELEGRAM_API in self.config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and self.check_required_config(\n            self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TELEGRAM_API]) \\\n               and self.get_is_enabled(self.config)\n\n    async def send_message_as_user(self, content, markdown=False, reply_to_message_id=None) -> telegram.Message:\n        kwargs = {}\n        if markdown:\n            kwargs[services_constants.MESSAGE_PARSE_MODE] = telegram.parsemode.ParseMode.MARKDOWN\n        try:\n            if content:\n                return await self.telegram_client.send_message(entity=self.user_account.username,\n                                                               message=content,\n                                                               reply_to=reply_to_message_id, **kwargs)\n        except Exception as e:\n            self.logger.error(f\"Failed to send message : {e}\")\n        return None\n\n    async def download_media_from_message(self, message, source=\"\"):\n        downloads_folder = os.path.join(self.tentacle_resources_path, self.DOWNLOADS_FOLDER, source)\n        if not os.path.exists(downloads_folder):\n            os.makedirs(downloads_folder)\n        await self.telegram_client.download_media(message=message, file=downloads_folder)\n        return downloads_folder\n\n    def get_successful_startup_message(self):\n        try:\n            return f\"Successfully connected to {self.user_account.username} account.\", True\n        except Exception as e:\n            self.logger.error(f\"Error when connecting to Telegram API ({e}): invalid telegram configuration.\")\n            return \"\", False\n"
  },
  {
    "path": "Services/Services_bases/telegram_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .telegram import TelegramService"
  },
  {
    "path": "Services/Services_bases/telegram_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/telegram_service/telegram.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport logging\nimport typing\nimport telegram\nimport telegram.ext\nimport telegram.request\nimport telegram.error\n\nimport octobot_commons.logging as bot_logging\nimport octobot_services.constants as services_constants\nimport octobot_services.enums as services_enums\nimport octobot_services.services as services\nimport octobot.constants as constants\n\n\nclass TelegramService(services.AbstractService):\n    CONNECT_TIMEOUT = 7  # default is 5, use 7 to take slow connections into account\n    CHAT_ID = \"chat-id\"\n    LOGGERS = [\"telegram._bot\", \"telegram.ext.Updater\", \"telegram.ext.ExtBot\",\n               \"hpack.hpack\", \"hpack.table\"]\n\n    def __init__(self):\n        super().__init__()\n        self.telegram_app: telegram.ext.Application = None\n        self._has_bot = False\n        self.chat_id = None\n        self.users = []\n        self.text_chat_dispatcher = {}\n        self._bot_url = None\n        self.connected = False\n\n    def get_fields_description(self):\n        return {\n            self.CHAT_ID: \"ID of your chat.\",\n            services_constants.CONFIG_TOKEN: \"Token given by 'botfather'.\",\n            services_constants.CONFIG_USERNAMES_WHITELIST: \"List of telegram usernames (user's @ identifier without \"\n                                                           \"@) allowed to talk to your OctoBot. This allows you to \"\n                                                           \"limit your OctoBot's telegram interactions to specific \"\n                                                           \"users only. No access restriction if left empty.\"\n        }\n\n    def get_default_value(self):\n        return {\n            self.CHAT_ID: \"\",\n            services_constants.CONFIG_TOKEN: \"\",\n            services_constants.CONFIG_USERNAMES_WHITELIST: [],\n        }\n\n    def get_required_config(self):\n        return [self.CHAT_ID, services_constants.CONFIG_TOKEN]\n\n    def get_read_only_info(self) -> list[services.ReadOnlyInfo]:\n        return [\n            services.ReadOnlyInfo(\n                'Connected to:', self._bot_url, services_enums.ReadOnlyInfoType.CLICKABLE\n            )\n        ] if self._bot_url else []\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/telegram\"\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return services_constants.CONFIG_TELEGRAM in config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and services_constants.CONFIG_SERVICE_INSTANCE in config[services_constants.CONFIG_CATEGORY_SERVICES][\n                   services_constants.CONFIG_TELEGRAM]\n\n    async def prepare(self):\n        if not self.telegram_app:\n            bot_logging.set_logging_level(self.LOGGERS, logging.WARNING)\n            self.chat_id = self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TELEGRAM][\n                self.CHAT_ID]\n            # force http 1.1 requests to avoid the following issue:\n            # Invalid input ConnectionInputs.RECV_WINDOW_UPDATE in state ConnectionState.CLOSED\n            # from https://github.com/python-telegram-bot/python-telegram-bot/issues/3556\n            self.telegram_app = telegram.ext.ApplicationBuilder()\\\n                .token(\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TELEGRAM][\n                    services_constants.CONFIG_TOKEN]\n                )\\\n                .request(telegram.request.HTTPXRequest(\n                    connect_timeout=self.CONNECT_TIMEOUT\n                ))\\\n                .get_updates_request(telegram.request.HTTPXRequest(\n                    connect_timeout=self.CONNECT_TIMEOUT\n                ))\\\n                .build()\n            try:\n                await self._start_app()\n            except telegram.error.InvalidToken as e:\n                self.logger.error(f\"Telegram configuration error: {e} Your Telegram token is invalid.\")\n            except telegram.error.NetworkError as e:\n                self.log_connection_error_message(e)\n\n    async def _start_app(self):\n        self.logger.debug(\"Initializing telegram connection\")\n        self.connected = True\n        await self.telegram_app.initialize()\n        if self.telegram_app.post_init:\n            await self.telegram_app.post_init(self.telegram_app)\n\n    async def _start_bot(self, polling_error_callback):\n        self._has_bot = True\n        await self.telegram_app.updater.start_polling(error_callback=polling_error_callback)\n        await self.telegram_app.start()\n\n    async def _stop_app(self):\n        await self.telegram_app.shutdown()\n        if self.telegram_app.post_shutdown:\n            await self.telegram_app.post_shutdown(self.telegram_app)\n        self.connected = False\n\n    async def _stop_bot(self):\n        if self.telegram_app.updater.running:\n            # await self.telegram_app.updater.shutdown()\n            try:\n                await self.telegram_app.updater.stop()\n            except telegram.error.TimedOut as err:\n                # can happen, ignore error\n                self.logger.debug(f\"Ignored {err} when stopping telegram bot\")\n        if self.telegram_app.running:\n            await self.telegram_app.stop()\n        if self.telegram_app.post_stop:\n            await self.telegram_app.post_stop(self.telegram_app)\n        self._has_bot = False\n\n    def register_text_polling_handler(self, chat_types: telegram.constants.ChatType, handler):\n        for chat_type in chat_types:\n            self.text_chat_dispatcher[chat_type] = handler\n\n    async def text_handler(self, update: telegram.Update, context: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        chat_type = update.effective_chat.type\n        if chat_type in self.text_chat_dispatcher:\n            await self.text_chat_dispatcher[chat_type](update, context)\n        else:\n            self.logger.info(f\"No handler for telegram update of type {chat_type}, update: {update}\")\n\n    def add_text_handler(self):\n        self.telegram_app.add_handler(\n            telegram.ext.MessageHandler(telegram.ext.filters.TEXT, self.text_handler)\n        )\n\n    def add_handlers(self, handlers):\n        self.telegram_app.add_handlers(handlers)\n\n    def add_error_handler(self, handler):\n        self.telegram_app.add_error_handler(handler)\n\n    def is_registered(self, user_key):\n        return user_key in self.users\n\n    def register_user(self, user_key):\n        self.users.append(user_key)\n\n    async def start_bot(self, polling_error_callback):\n        try:\n            if not self._has_bot and self.users:\n                await self._start_bot(polling_error_callback)\n                self.logger.debug(\"Started telegram bot\")\n                self.add_text_handler()\n        except Exception as e:\n            raise e\n\n    def is_running(self):\n        return self.telegram_app and self.telegram_app.running\n\n    def get_type(self):\n        return services_constants.CONFIG_TELEGRAM\n\n    def get_website_url(self):\n        return \"https://telegram.org/\"\n\n    def get_endpoint(self):\n        return self.telegram_app\n\n    async def stop(self):\n        if self.connected:\n            if self._has_bot:\n                await self._stop_bot()\n            await self._stop_app()\n\n    @staticmethod\n    def get_is_enabled(config):\n        return services_constants.CONFIG_CATEGORY_SERVICES in config \\\n               and services_constants.CONFIG_TELEGRAM in config[services_constants.CONFIG_CATEGORY_SERVICES]\n\n    def has_required_configuration(self):\n        return services_constants.CONFIG_CATEGORY_SERVICES in self.config \\\n               and services_constants.CONFIG_TELEGRAM in self.config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and self.check_required_config(\n            self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TELEGRAM]) \\\n               and self.get_is_enabled(self.config)\n\n    async def send_message(self, content, markdown=False, reply_to_message_id=None) -> typing.Optional[telegram.Message]:\n        if not self.chat_id:\n            self.logger.warning(\n                \"Impossible to send telegram message: please provide a chat id in telegram configuration.\"\n            )\n            return None\n        kwargs = {}\n        if markdown:\n            kwargs[services_constants.MESSAGE_PARSE_MODE] = telegram.constants.ParseMode.MARKDOWN\n        try:\n            if content:\n                return await self.telegram_app.bot.send_message(\n                    chat_id=self.chat_id, text=content, reply_to_message_id=reply_to_message_id, **kwargs\n                )\n        except telegram.error.TimedOut:\n            # retry on failing\n            try:\n                return await self.telegram_app.bot.send_message(\n                    chat_id=self.chat_id, text=content, reply_to_message_id=reply_to_message_id, **kwargs\n                )\n            except telegram.error.TimedOut as e:\n                self.logger.error(f\"Failed to send message : {e}\")\n        except telegram.error.InvalidToken as e:\n            self.logger.error(f\"Failed to send message ({e}): invalid telegram configuration.\")\n        return None\n\n    def _fetch_bot_url(self):\n        self._bot_url = f\"https://web.telegram.org/#/im?p={self.telegram_app.bot.name}\"\n        return self._bot_url\n\n    def get_successful_startup_message(self):\n        try:\n            self.telegram_app.bot.name\n        except RuntimeError:\n            # raised by telegram_app.bot.name property when not properly initialized (invalid token, etc)\n            # error has already been logged in prepare()\n            return \"\", False\n        return f\"Successfully initialized and accessible at: {self._fetch_bot_url()}.\", True\n"
  },
  {
    "path": "Services/Services_bases/trading_view_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .trading_view import TradingViewService\n"
  },
  {
    "path": "Services/Services_bases/trading_view_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TradingViewService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/trading_view_service/trading_view.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport hashlib\nimport uuid\n\nimport octobot_commons.authentication as authentication\nimport octobot_services.constants as services_constants\nimport octobot_services.enums as services_enums\nimport octobot_services.services as services\nimport octobot.constants as constants\n\n\nclass TradingViewService(services.AbstractService):\n    def __init__(self):\n        super().__init__()\n        self.requires_token = None\n        self.token = None\n        self.use_email_alert = None\n        self._webhook_url = None\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return True\n\n    @staticmethod\n    def get_is_enabled(config):\n        return True\n\n    def has_required_configuration(self):\n        return True\n\n    def get_required_config(self):\n        return [\n            services_constants.CONFIG_REQUIRE_TRADING_VIEW_TOKEN,\n            services_constants.CONFIG_TRADING_VIEW_TOKEN,\n            # disabled until TradingView email alerts are restored\n            # services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS\n        ]\n\n    def get_fields_description(self):\n        return {\n            services_constants.CONFIG_REQUIRE_TRADING_VIEW_TOKEN: \"When enabled the TradingView webhook will require your \"\n                                                                  \"tradingview.com token to process any signal.\",\n            services_constants.CONFIG_TRADING_VIEW_TOKEN: \"Your personal unique tradingview.com token. Can be used to ensure only your \"\n                                                          \"TradingView signals are triggering your OctoBot in case someone else get \"\n                                                          \"your webhook link. You can change it at any moment but remember to change it \"\n                                                          \"on your tradingview.com signal account as well.\",\n            services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS: (\n                f\"When enabled, your OctoBot will trade using the free TradingView email alerts. When disabled, \"\n                f\"a webhook configuration is required to trade using TradingView alerts. Requires the \"\n                f\"{constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME}.\"\n            ),\n        }\n\n    def get_default_value(self):\n        return {\n            services_constants.CONFIG_REQUIRE_TRADING_VIEW_TOKEN: False,\n            services_constants.CONFIG_TRADING_VIEW_TOKEN: self.get_security_token(uuid.uuid4().hex),\n            # disabled until TradingView email alerts are restored\n            # services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS: False,\n        }\n\n    def is_improved_by_extensions(self) -> bool:\n        return True\n\n    def get_read_only_info(self) -> list[services.ReadOnlyInfo]:\n        read_only_info = []\n        auth = authentication.Authenticator.instance()\n        if auth.is_tradingview_email_confirmed() and (email_address := auth.get_saved_tradingview_email()):\n            read_only_info.append(services.ReadOnlyInfo(\n                'Email address:', email_address, services_enums.ReadOnlyInfoType.COPYABLE,\n                configuration_title=\"Configure on TradingView\", configuration_path=\"tradingview_email_config\"\n            ))\n        else:\n            pass\n            # disabled until TradingView email alerts are restored\n            # read_only_info.append(services.ReadOnlyInfo(\n            #     'Email address:', \"Generate email\", services_enums.ReadOnlyInfoType.CTA,\n            #     path=\"tradingview_email_config\"\n            # ))\n        if self._webhook_url:\n            read_only_info.append(services.ReadOnlyInfo(\n                'Webhook url:',\n                self._webhook_url,\n                services_enums.ReadOnlyInfoType.READONLY\n                if self._webhook_url == services_constants.TRADING_VIEW_USING_EMAIL_INSTEAD_OF_WEBHOOK\n                else services_enums.ReadOnlyInfoType.COPYABLE,\n            ))\n        return read_only_info\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/tradingview\"\n\n    def get_endpoint(self) -> None:\n        return None\n\n    def get_type(self) -> None:\n        return services_constants.CONFIG_TRADING_VIEW\n\n    def get_website_url(self):\n        return \"https://www.tradingview.com/?aff_id=27595\"\n\n    def get_logo(self):\n        return \"https://in.tradingview.com/static/images/favicon.ico\"\n\n    async def prepare(self) -> None:\n        try:\n            self.requires_token = \\\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TRADING_VIEW][\n                    services_constants.CONFIG_REQUIRE_TRADING_VIEW_TOKEN]\n            self.token = \\\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TRADING_VIEW][\n                    services_constants.CONFIG_TRADING_VIEW_TOKEN]\n            self.use_email_alert = \\\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TRADING_VIEW].get(\n                    services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS, False\n                )\n        except KeyError:\n            if self.requires_token is None:\n                self.requires_token = self.get_default_value()[services_constants.CONFIG_REQUIRE_TRADING_VIEW_TOKEN]\n            if self.token is None:\n                self.token = self.get_default_value()[services_constants.CONFIG_TRADING_VIEW_TOKEN]\n            if self.use_email_alert is None:\n                self.use_email_alert = self.get_default_value().get(services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS, False)\n            # save new values into config file\n            updated_config = {\n                services_constants.CONFIG_REQUIRE_TRADING_VIEW_TOKEN: self.requires_token,\n                services_constants.CONFIG_TRADING_VIEW_TOKEN: self.token,\n            }\n            if self.use_email_alert:\n                # only save CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS if use_email_alert is True\n                # (to keep the option of users still using it)\n                updated_config[services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS] = self.use_email_alert\n            self.save_service_config(services_constants.CONFIG_TRADING_VIEW, updated_config)\n\n    @staticmethod\n    def get_security_token(pin_code):\n        \"\"\"\n        Generate unique token from pin.  This adds a marginal amount of security.\n        :param pin_code: the pin code to use\n        :return: the generated token\n        \"\"\"\n        token = hashlib.sha224(pin_code.encode('utf-8'))\n        return token.hexdigest()\n\n    def register_webhook_url(self, webhook_url):\n        self._webhook_url = webhook_url\n\n    def get_successful_startup_message(self):\n        return \"\", True\n"
  },
  {
    "path": "Services/Services_bases/twitter_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .twitter import TwitterService"
  },
  {
    "path": "Services/Services_bases/twitter_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TwitterService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/twitter_service/twitter.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport requests\n\n# comment imports to remove twitter from dependencies when tentacle is disabled\n# import twitter\n# import twitter.api\n# import twitter.twitter_utils\n\nimport octobot_services.constants as services_constants\nimport octobot_services.enums as services_enums\nimport octobot_services.services as services\nimport octobot.constants as constants\n\n\n# disable inheritance to disable tentacle visibility. Disabled as starting from feb 9 2023, API is now paid only\n# class TwitterService(services.AbstractService):\nclass TwitterService:\n    API_KEY = \"api-key\"\n    API_SECRET = \"api-secret\"\n    ACCESS_TOKEN = \"access-token\"\n    ACCESS_TOKEN_SECRET = \"access-token-secret\"\n\n    def __init__(self):\n        super().__init__()\n        self.twitter_api = None\n        self._account_url = None\n\n    def get_fields_description(self):\n        return {\n            self.API_KEY: \"Your Twitter API key.\",\n            self.API_SECRET: \"Your Twitter API-secret key.\",\n            self.ACCESS_TOKEN: \"Your Twitter access token key.\",\n            self.ACCESS_TOKEN_SECRET: \"Your Twitter access token secret key.\"\n        }\n\n    def get_default_value(self):\n        return {\n            self.API_KEY: \"\",\n            self.API_SECRET: \"\",\n            self.ACCESS_TOKEN: \"\",\n            self.ACCESS_TOKEN_SECRET: \"\"\n        }\n\n    def get_required_config(self):\n        return [self.API_KEY, self.API_SECRET, self.ACCESS_TOKEN, self.ACCESS_TOKEN_SECRET]\n\n    def get_read_only_info(self) -> list[services.ReadOnlyInfo]:\n        return [\n            services.ReadOnlyInfo(\n                'Connected to:', self._account_url, services_enums.ReadOnlyInfoType.CLICKABLE\n            )\n        ] if self._account_url else []\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/twitter\"\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return services_constants.CONFIG_TWITTER in config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and services_constants.CONFIG_SERVICE_INSTANCE in config[services_constants.CONFIG_CATEGORY_SERVICES][\n                   services_constants.CONFIG_TWITTER]\n\n    def get_user_id(self, user_account):\n        user = self.twitter_api.GetUser(screen_name=user_account)\n        return user.id\n\n    def get_history(self, user_id):\n        return self.twitter_api.GetUserTimeline(user_id=user_id)\n\n    async def prepare(self):\n        if not self.twitter_api:\n            self.twitter_api = twitter.Api(\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TWITTER][\n                    self.API_KEY],\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TWITTER][\n                    self.API_SECRET],\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TWITTER][\n                    self.ACCESS_TOKEN],\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TWITTER][\n                    self.ACCESS_TOKEN_SECRET],\n                sleep_on_rate_limit=True\n            )\n\n    def get_type(self):\n        return services_constants.CONFIG_TWITTER\n\n    def get_website_url(self):\n        return \"https://twitter.com/\"\n\n    def get_endpoint(self):\n        return self.twitter_api\n\n    def has_required_configuration(self):\n        return services_constants.CONFIG_CATEGORY_SERVICES in self.config \\\n               and services_constants.CONFIG_TWITTER in self.config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and self.check_required_config(\n            self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_TWITTER])\n\n    @staticmethod\n    def decode_tweet(tweet):\n        if \"extended_tweet\" in tweet and \"full_text\" in tweet:\n            return tweet[\"extended_tweet\"][\"full_text\"]\n        elif \"text\" in tweet:\n            return tweet[\"text\"]\n        return \"\"\n\n    async def post(self, content, error_on_failure=True):\n        try:\n            return self.split_if_necessary_and_send_tweet(content=content, tweet_id=None)\n        except Exception as e:\n            error = f\"Failed to send tweet : {e} tweet:{content}\"\n            if error_on_failure:\n                self.logger.error(error)\n            else:\n                self.logger.info(error)\n        return None\n\n    async def respond(self, tweet_id, content, error_on_failure=True):\n        try:\n            return self.split_if_necessary_and_send_tweet(content=content, tweet_id=tweet_id)\n        except Exception as e:\n            error = f\"Failed to send response tweet : {e} tweet:{content}\"\n            if error_on_failure:\n                self.logger.error(error)\n            else:\n                self.logger.info(error)\n        return None\n\n    def split_if_necessary_and_send_tweet(self, content, counter=None, counter_max=None, tweet_id=None):\n        # add twitter counter at the beginning\n        if counter is not None and counter_max is not None:\n            content = f\"{counter}/{counter_max} {content}\"\n            counter += 1\n\n        # get the current content size\n        post_size = twitter.twitter_utils.calc_expected_status_length(content)\n\n        # check if the current content size can be posted\n        if post_size > twitter.api.CHARACTER_LIMIT:\n\n            # calculate the number of post required for the whole content\n            if not counter_max:\n                counter_max = post_size // twitter.api.CHARACTER_LIMIT\n                counter = 1\n\n            # post the current tweet\n            # no async call possible yet\n            post = self.twitter_api.PostUpdate(status=content[:twitter.api.CHARACTER_LIMIT],\n                                               in_reply_to_status_id=tweet_id)\n\n            # recursive call for all post while content > twitter.api.CHARACTER_LIMIT\n            self.split_if_necessary_and_send_tweet(content[twitter.api.CHARACTER_LIMIT:],\n                                                   counter=counter,\n                                                   counter_max=counter_max,\n                                                   tweet_id=tweet_id)\n\n            return post\n        else:\n            return self.twitter_api.PostUpdate(status=content[:twitter.api.CHARACTER_LIMIT],\n                                               in_reply_to_status_id=tweet_id)\n\n    def get_tweet_text(self, tweet):\n        try:\n            return TwitterService.decode_tweet(tweet)\n        except Exception as e2:\n            self.logger.error(e2)\n        return \"\"\n\n    @staticmethod\n    def get_twitter_id_from_url(url):\n        return str(url).split(\"/\")[-1]\n\n    def get_tweet(self, tweet_id):\n        return self.twitter_api.GetStatus(tweet_id)\n\n    def _fetch_twitter_url(self):\n        self._account_url = f\"https://twitter.com/{self.twitter_api.VerifyCredentials().screen_name}\"\n        return self._account_url\n\n    def get_successful_startup_message(self):\n        try:\n            return f\"Successfully initialized and accessible at: {self._fetch_twitter_url()}.\", True\n        except requests.exceptions.ConnectionError as e:\n            self.log_connection_error_message(e)\n            return \"\", False\n"
  },
  {
    "path": "Services/Services_bases/web_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .web import WebService"
  },
  {
    "path": "Services/Services_bases/web_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"WebService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/web_service/web.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport os\nimport socket\n\nimport octobot_commons.constants as commons_constants\nimport octobot_services.constants as services_constants\nimport octobot_services.services as services\nimport octobot.constants as constants\n\n\nLOCAL_HOST_IP = \"127.0.0.1\"\n\n\nclass WebService(services.AbstractService):\n    BACKTESTING_ENABLED = True\n\n    def __init__(self):\n        super().__init__()\n        self.web_app = None\n        self.requires_password = None\n        self.password_hash = None\n\n    def get_fields_description(self):\n        return {\n            services_constants.CONFIG_WEB_PORT: \"Port to access your OctoBot web interface from.\",\n            services_constants.CONFIG_AUTO_OPEN_IN_WEB_BROWSER: \"When enabled, OctoBot will open the web interface on your web \"\n                                                                \"browser upon startup.\",\n            services_constants.CONFIG_WEB_REQUIRES_PASSWORD: \"When enabled, OctoBot web interface will be protected by a password. \"\n                                                             \"Failing 10 times to enter this password will block the user and require \"\n                                                             \"OctoBot to restart before being able to retry to authenticate.\",\n            services_constants.CONFIG_WEB_PASSWORD: \"Password to enter to access this OctoBot when password protection is enabled. \"\n                                                    \"Only a hash of this password will be stored.\"\n        }\n\n    def get_default_value(self):\n        return {\n            services_constants.CONFIG_WEB_PORT: services_constants.DEFAULT_SERVER_PORT,\n            services_constants.CONFIG_AUTO_OPEN_IN_WEB_BROWSER: True,\n            services_constants.CONFIG_WEB_REQUIRES_PASSWORD: False,\n            services_constants.CONFIG_WEB_PASSWORD: \"\"\n        }\n\n    def get_required_config(self):\n        return [services_constants.CONFIG_WEB_PORT]\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/web\"\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return services_constants.CONFIG_WEB in config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and services_constants.CONFIG_SERVICE_INSTANCE in config[services_constants.CONFIG_CATEGORY_SERVICES][\n                   services_constants.CONFIG_WEB]\n\n    @staticmethod\n    def get_is_enabled(config):\n        # allow to disable web interface from config, enabled by default otherwise\n        try:\n            return config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEB][\n                commons_constants.CONFIG_ENABLED_OPTION]\n        except KeyError:\n            return True\n\n    def has_required_configuration(self):\n        return self.get_is_enabled(self.config)\n\n    def get_endpoint(self) -> None:\n        return self.web_app\n\n    def get_type(self) -> None:\n        return services_constants.CONFIG_WEB\n\n    def get_website_url(self):\n        return \"/home\"\n\n    def get_logo(self):\n        return \"static/img/svg/octobot.svg\"\n\n    async def prepare(self) -> None:\n        try:\n            self.requires_password = \\\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEB][\n                    services_constants.CONFIG_WEB_REQUIRES_PASSWORD]\n            self.password_hash = \\\n            self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEB][\n                services_constants.CONFIG_WEB_PASSWORD]\n        except KeyError:\n            if self.requires_password is None:\n                self.requires_password = self.get_default_value()[services_constants.CONFIG_WEB_REQUIRES_PASSWORD]\n            if self.password_hash is None:\n                self.password_hash = self.get_default_value()[services_constants.CONFIG_WEB_PASSWORD]\n            # save new values into config file\n            updated_config = {\n                services_constants.CONFIG_WEB_REQUIRES_PASSWORD: self.requires_password,\n                services_constants.CONFIG_WEB_PASSWORD: self.password_hash\n            }\n            self.save_service_config(services_constants.CONFIG_WEB, updated_config, update=True)\n\n    @staticmethod\n    def get_should_warn():\n        return False\n\n    async def stop(self):\n        if self.web_app:\n            self.web_app.stop()\n\n    def _get_web_server_port(self):\n        try:\n            return os.getenv(\n                services_constants.ENV_WEB_PORT,\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEB][\n                    services_constants.CONFIG_WEB_PORT]\n            )\n        except KeyError:\n            return os.getenv(services_constants.ENV_WEB_PORT, services_constants.DEFAULT_SERVER_PORT)\n\n    def _get_web_server_url(self):\n        port = self._get_web_server_port()\n        try:\n            return f\"{os.getenv(services_constants.ENV_WEB_ADDRESS, socket.gethostbyname(socket.gethostname()))}:{port}\"\n        except OSError as err:\n            self.logger.warning(\n                f\"Impossible to find local web interface url, using default instead: {err} ({err.__class__.__name__})\"\n            )\n        # use localhost by default\n        return f\"{LOCAL_HOST_IP}:{port}\"\n\n    def get_successful_startup_message(self):\n        return f\"Interface successfully initialized and accessible at: http://{self._get_web_server_url()}.\", True\n"
  },
  {
    "path": "Services/Services_bases/webhook_service/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .webhook import WebHookService\n"
  },
  {
    "path": "Services/Services_bases/webhook_service/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"WebHookService\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Services/Services_bases/webhook_service/webhook.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport logging\nimport os\nimport time\nimport flask\nimport threading\nimport gevent.pywsgi\nimport pyngrok.ngrok as ngrok\nimport pyngrok.exception\n\nimport octobot_commons.logging as bot_logging\nimport octobot_commons.configuration as configuration\nimport octobot_commons.authentication as authentication\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_services.constants as services_constants\nimport octobot_services.services as services\nimport octobot.constants as constants\nimport octobot.community.errors as community_errors\n\n\nclass WebHookService(services.AbstractService):\n    CONNECTION_TIMEOUT = 8  # can take up to 5s on slow setups\n    LOGGERS = [\"pyngrok.ngrok\", \"werkzeug\"]\n\n    def get_fields_description(self):\n        if self.use_web_interface_for_webhook:\n            return {}\n        return {\n            services_constants.CONFIG_ENABLE_OCTOBOT_WEBHOOK:\n                f\"Use OctoBot cloud webhook. Requires the {constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME}.\",\n            services_constants.CONFIG_ENABLE_NGROK: \"Use Ngrok\",\n            services_constants.CONFIG_NGROK_TOKEN: \"The ngrok token used to expose the webhook to the internet.\",\n            services_constants.CONFIG_NGROK_DOMAIN: \"[Optional] The ngrok subdomain.\",\n            services_constants.CONFIG_WEBHOOK_SERVER_IP: \"WebHook bind IP: used for webhook when ngrok is not enabled.\",\n            services_constants.CONFIG_WEBHOOK_SERVER_PORT: \"WebHook port: used for webhook when ngrok is not enabled.\"\n        }\n\n    def get_default_value(self):\n        if self.use_web_interface_for_webhook:\n            return {}\n        return {\n            services_constants.CONFIG_ENABLE_OCTOBOT_WEBHOOK: False,\n            services_constants.CONFIG_ENABLE_NGROK: True,\n            services_constants.CONFIG_NGROK_TOKEN: \"\",\n            services_constants.CONFIG_NGROK_DOMAIN: \"\",\n            services_constants.CONFIG_WEBHOOK_SERVER_IP: services_constants.DEFAULT_WEBHOOK_SERVER_IP,\n            services_constants.CONFIG_WEBHOOK_SERVER_PORT: services_constants.DEFAULT_WEBHOOK_SERVER_PORT\n        }\n\n    def is_improved_by_extensions(self) -> bool:\n        return True\n\n    def __init__(self):\n        super().__init__()\n        self.use_web_interface_for_webhook = constants.IS_CLOUD_ENV\n        self.use_octobot_cloud_webhook = False\n        self.use_octobot_cloud_email_webhook = False\n        self.ngrok_tunnel = None\n        self.webhook_public_url = \"\"\n        self.ngrok_enabled = True\n        self.ngrok_domain = None\n\n        self.service_feed_webhooks = {}\n        self.service_feed_auth_callbacks = {}\n\n        self.webhook_app = None\n        self.webhook_host = None\n        self.webhook_port = None\n        self.webhook_server = None\n        self.webhook_server_context = None\n        self.webhook_server_thread = None\n        self.connected = None\n\n    @staticmethod\n    def is_setup_correctly(config):\n        return services_constants.CONFIG_WEBHOOK in config[services_constants.CONFIG_CATEGORY_SERVICES] \\\n               and services_constants.CONFIG_SERVICE_INSTANCE in config[services_constants.CONFIG_CATEGORY_SERVICES][\n                   services_constants.CONFIG_WEBHOOK]\n\n    @staticmethod\n    def get_is_enabled(config):\n        return True\n\n    def check_required_config(self, config):\n        if self.use_web_interface_for_webhook:\n            return True\n        if self.is_using_octobot_cloud_webhook() or self.is_using_octobot_cloud_email_webhook():\n            return True\n        try:\n            token = config.get(services_constants.CONFIG_NGROK_TOKEN)\n            enabled_ngrok = config.get(services_constants.CONFIG_ENABLE_NGROK, True)\n            if enabled_ngrok:\n                return token and not configuration.has_invalid_default_config_value(token)\n            return not (\n                configuration.has_invalid_default_config_value(\n                    config.get(services_constants.CONFIG_WEBHOOK_SERVER_PORT)\n                ) or configuration.has_invalid_default_config_value(\n                    config.get(services_constants.CONFIG_WEBHOOK_SERVER_IP)\n                )\n            )\n        except KeyError:\n            return False\n\n    def has_required_configuration(self):\n        try:\n            return self.check_required_config(self.get_webhook_config())\n        except KeyError:\n            return False\n\n    def is_using_octobot_cloud_webhook(self):\n        return self.get_webhook_config().get(services_constants.CONFIG_ENABLE_OCTOBOT_WEBHOOK)\n\n    def is_using_octobot_cloud_email_webhook(self):\n        return self.config[services_constants.CONFIG_CATEGORY_SERVICES].get(\n            services_constants.CONFIG_TRADING_VIEW, {}\n        ).get(\n            services_constants.CONFIG_TRADING_VIEW_USE_EMAIL_ALERTS, False\n        )\n\n    def get_webhook_config(self):\n        return self.config[services_constants.CONFIG_CATEGORY_SERVICES].get(services_constants.CONFIG_WEBHOOK, {})\n\n    def get_required_config(self):\n        return [] if self.use_web_interface_for_webhook else \\\n            [services_constants.CONFIG_ENABLE_NGROK, services_constants.CONFIG_NGROK_TOKEN]\n\n    @classmethod\n    def get_help_page(cls) -> str:\n        return f\"{constants.OCTOBOT_DOCS_URL}/octobot-interfaces/tradingview/using-a-webhook\"\n\n    def get_type(self) -> None:\n        return services_constants.CONFIG_WEBHOOK\n\n    def get_logo(self):\n        return None\n\n    def is_subscribed(self, feed_name):\n        return feed_name in self.service_feed_webhooks\n\n    @staticmethod\n    def connect(port, protocol=\"http\", domain=None) -> ngrok.NgrokTunnel:\n        \"\"\"\n        Create a new ngrok tunnel\n        :param port: the tunnel local port\n        :param protocol: the protocol to use\n        :return: the ngrok url\n        \"\"\"\n        return ngrok.connect(port, protocol, domain=domain)\n\n    def subscribe_feed(self, service_feed_name, service_feed_callback, auth_callback) -> None:\n        \"\"\"\n        Subscribe a service feed to the webhook\n        :param service_feed_name: the service feed name\n        :param service_feed_callback: the service feed callback reference\n        :return: the service feed webhook url\n        \"\"\"\n        if service_feed_name not in self.service_feed_webhooks:\n            self.service_feed_webhooks[service_feed_name] = service_feed_callback\n            self.service_feed_auth_callbacks[service_feed_name] = auth_callback\n            return\n        raise KeyError(f\"Service feed has already subscribed to a webhook : {service_feed_name}\")\n\n    def get_subscribe_url(self, service_feed_name):\n        if self.use_octobot_cloud_email_webhook:\n            return services_constants.TRADING_VIEW_USING_EMAIL_INSTEAD_OF_WEBHOOK\n        if self.use_octobot_cloud_webhook:\n            return self._get_community_feed_webhook_url()\n        return f\"{self.webhook_public_url}/{service_feed_name}\"\n\n    def _prepare_webhook_server(self):\n        try:\n            self.logger.debug(f\"Starting local webhook server at {self.webhook_host}:{self.webhook_port}\")\n            self.webhook_server = gevent.pywsgi.WSGIServer(\n                (self.webhook_host, self.webhook_port),\n                self.webhook_app,\n                log=None\n            )\n            self.webhook_server_context = self.webhook_app.app_context()\n            self.webhook_server_context.push()\n        except OSError as e:\n            self.webhook_server = None\n            self.logger.exception(e, False, f\"Fail to start webhook : {e}\")\n\n    def _register_webhook_routes(self, blueprint) -> None:\n        @blueprint.route('/')\n        def index():\n            \"\"\"\n            Route to check if webhook server is online\n            \"\"\"\n            return ''\n\n        @blueprint.route('/webhook/<webhook_name>', methods=['POST'])\n        def webhook(webhook_name):\n            return self._flask_webhook_call(webhook_name)\n\n    def _flask_webhook_call(self, webhook_name):\n        if flask.request.method == 'POST':\n            data = flask.request.get_data(as_text=True)\n            if self._default_webhook_call(webhook_name, data):\n                return '', 200\n            return 'invalid or missing input parameters', 400\n        flask.abort(405)\n\n    def _community_webhook_call_factory(self, service_name: str):\n\n        async def _community_webhook_callback(data: dict) -> bool:\n            return await self._async_default_webhook_call(\n                service_name, data[commons_enums.CommunityFeedAttrs.VALUE.value]\n            )\n\n        return _community_webhook_callback\n\n    def _default_webhook_call(self, webhook_name: str, data: str) -> bool:\n        if self.is_valid_webhook_call(webhook_name, data):\n            self.service_feed_webhooks[webhook_name](data)\n            return True\n        return False\n\n    async def _async_default_webhook_call(self, webhook_name: str, data: str) -> bool:\n        if self.is_valid_webhook_call(webhook_name, data):\n            await self.service_feed_webhooks[webhook_name](data)\n            return True\n        return False\n\n    def is_valid_webhook_call(self, webhook_name:str , data: str):\n        if webhook_name in self.service_feed_webhooks:\n            if self.service_feed_auth_callbacks[webhook_name](data):\n                return True\n            else:\n                self.logger.warning(f\"Ignored message (wrong token): {data}\")\n                return False\n        self.logger.warning(f\"Received unknown request from {webhook_name}\")\n        return False\n\n    def is_using_cloud_webhooks(self):\n        return self.use_octobot_cloud_webhook or self.use_octobot_cloud_email_webhook\n\n    async def prepare(self) -> None:\n        if self.use_web_interface_for_webhook:\n            return\n        if self.is_using_octobot_cloud_email_webhook():\n            self.use_octobot_cloud_email_webhook = True\n            return\n        if self.is_using_octobot_cloud_webhook():\n            self.use_octobot_cloud_webhook = True\n            return\n        bot_logging.set_logging_level(self.LOGGERS, logging.WARNING)\n        self.ngrok_enabled = self.config[services_constants.CONFIG_CATEGORY_SERVICES][\n            services_constants.CONFIG_WEBHOOK].get(services_constants.CONFIG_ENABLE_NGROK, True)\n        if self.ngrok_enabled:\n            ngrok.set_auth_token(\n                self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEBHOOK][\n                    services_constants.CONFIG_NGROK_TOKEN])\n        self.ngrok_domain = self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_WEBHOOK]\\\n            .get(services_constants.CONFIG_NGROK_DOMAIN, None)\n        if self.ngrok_domain in commons_constants.DEFAULT_CONFIG_VALUES:\n            # ignore default values\n            self.ngrok_domain = None\n        try:\n            self.webhook_host = os.getenv(services_constants.ENV_WEBHOOK_ADDRESS,\n                                          self.config[services_constants.CONFIG_CATEGORY_SERVICES]\n                                          [services_constants.CONFIG_WEBHOOK][services_constants.CONFIG_WEBHOOK_SERVER_IP])\n        except KeyError:\n            self.webhook_host = os.getenv(services_constants.ENV_WEBHOOK_ADDRESS,\n                                          services_constants.DEFAULT_WEBHOOK_SERVER_IP)\n        try:\n            self.webhook_port = int(\n                os.getenv(services_constants.ENV_WEBHOOK_PORT, self.config[services_constants.CONFIG_CATEGORY_SERVICES]\n                [services_constants.CONFIG_WEBHOOK][services_constants.CONFIG_WEBHOOK_SERVER_PORT]))\n        except KeyError:\n            self.webhook_port = int(\n                os.getenv(services_constants.ENV_WEBHOOK_PORT, services_constants.DEFAULT_WEBHOOK_SERVER_PORT))\n\n    def _start_server(self):\n        try:\n            self._prepare_webhook_server()\n            self._register_webhook_routes(self.webhook_app)\n            self.webhook_public_url = f\"http://{self.webhook_host}:{self.webhook_port}/webhook\"\n            if self.ngrok_enabled:\n                self.ngrok_tunnel = self.connect(self.webhook_port, protocol=\"http\", domain=self.ngrok_domain)\n                self.webhook_public_url = f\"{self.ngrok_tunnel.public_url}/webhook\"\n            if self.webhook_server:\n                self.connected = True\n                self.webhook_server.serve_forever()\n        except pyngrok.exception.PyngrokNgrokError as e:\n            self.logger.error(f\"Error when starting webhook service: Your ngrok.com token might be invalid. ({e})\")\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when running webhook service: ({e})\")\n        self.connected = False\n\n    async def _start_isolated_server(self):\n        if self.webhook_app is None:\n            self.webhook_app = flask.Flask(__name__)\n            # gevent WSGI server has to be created in the thread it is started: create everything in this thread\n            self.webhook_server_thread = threading.Thread(target=self._start_server, name=self.get_name())\n            self.webhook_server_thread.start()\n            start_time = time.time()\n            timeout = False\n            while self.connected is None and not timeout:\n                time.sleep(0.1)\n                timeout = time.time() - start_time > self.CONNECTION_TIMEOUT\n            if timeout:\n                self.logger.error(\"Webhook took too long to start, now stopping it.\")\n                await self.stop()\n                self.connected = False\n            return self.connected is True\n        return True\n\n    async def _register_on_web_interface(self):\n        import tentacles.Services.Interfaces.web_interface.api as api\n        if not api.has_webhook(self._flask_webhook_call):\n            api.register_webhook(self._flask_webhook_call)\n        authenticator = authentication.Authenticator.instance()\n        if not authenticator.initialized_event.is_set():\n            await asyncio.wait_for(authenticator.initialized_event.wait(), authenticator.LOGIN_TIMEOUT)\n        try:\n            # deployed bot url\n            self.webhook_public_url = f\"{await authenticator.get_deployment_url()}/api/webhook\"\n            self.connected = True\n            return True\n        except community_errors.BotError as err:\n            self.logger.exception(err, True, f\"Impossible to start web interface based webhook {err}\")\n            return False\n\n    def _get_community_feed_webhook_url(self) -> str:\n        try:\n            authenticator = authentication.Authenticator.instance()\n            bot_identifier = authenticator.get_saved_mqtt_device_uuid()\n            return f\"{constants.COMMUNITY_TRADINGVIEW_WEBHOOK_BASE_URL}/{bot_identifier}\"\n        except community_errors.NoBotDeviceError:\n            return \"\"\n\n    async def _register_on_community_feed(self):\n        authenticator = authentication.Authenticator.instance()\n        bot_identifier = authenticator.get_saved_mqtt_device_uuid()\n        if not authenticator.initialized_event.is_set():\n            await asyncio.wait_for(authenticator.initialized_event.wait(), authenticator.LOGIN_TIMEOUT)\n        try:\n            for feed_name, channel_type in [\n                (services_constants.TRADINGVIEW_WEBHOOK_SERVICE_NAME, commons_enums.CommunityChannelTypes.TRADINGVIEW)\n            ]:\n                await authenticator.register_feed_callback(\n                    channel_type,\n                    self._community_webhook_call_factory(feed_name),\n                    identifier=bot_identifier\n                )\n            self.webhook_public_url = self._get_community_feed_webhook_url()\n            self.connected = True\n            return True\n        except community_errors.BotError as err:\n            self.logger.exception(err, True, f\"Impossible to start OctoBot cloud based webhook {err}\")\n            return False\n\n    async def start_webhooks(self) -> bool:\n        if self.use_web_interface_for_webhook:\n            return await self._register_on_web_interface()\n        if self.is_using_cloud_webhooks():\n            try:\n                return await self._register_on_community_feed()\n            except community_errors.NoBotDeviceError:\n                raise community_errors.ExtensionRequiredError(\n                    f\"A connected OctoBot account using the {constants.OCTOBOT_EXTENSION_PACKAGE_1_NAME} \"\n                    f\"is required to use OctoBot {'email' if self.use_octobot_cloud_email_webhook else 'webhook' } \"\n                    f\"alerts for TradingView.\"\n                )\n        return await self._start_isolated_server()\n\n    def _is_healthy(self):\n        return (\n            self.use_web_interface_for_webhook or\n            self.is_using_octobot_cloud_webhook() or\n            self.is_using_octobot_cloud_email_webhook() or\n            (self.webhook_host is not None and self.webhook_port is not None)\n        )\n\n    def get_successful_startup_message(self):\n        webhook_endpoint = f\"ngrok address\"\n        if self.use_web_interface_for_webhook:\n            webhook_endpoint = \"web interface webhook api\"\n        if self.is_using_octobot_cloud_webhook() or self.is_using_octobot_cloud_email_webhook():\n            webhook_endpoint = \"OctoBot cloud network\"\n        return f\"Webhook configured on {webhook_endpoint}\", self._is_healthy()\n\n    async def stop(self):\n        if not self.use_web_interface_for_webhook and self.connected:\n            ngrok.kill()\n            if self.webhook_server:\n                try:\n                    self.webhook_server.stop()\n                except Exception as err:\n                    self.logger.warning(f\"Error when stopping webhook server: {err}\")\n"
  },
  {
    "path": "Services/Services_feeds/google_service_feed/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .google_feed import GoogleServiceFeed\n    from .google_feed import TrendTopic\n"
  },
  {
    "path": "Services/Services_feeds/google_service_feed/google_feed.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport time\nimport aiohttp\n\nimport simplifiedpytrends.exceptions\nimport simplifiedpytrends.request\n\nimport octobot_services.channel as services_channel\nimport octobot_services.constants as services_constants\nimport octobot_services.service_feeds as service_feeds\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass GoogleServiceFeedChannel(services_channel.AbstractServiceFeedChannel):\n    pass\n\n\nclass TrendTopic:\n    def __init__(self, refresh_time, keywords, category=0, time_frame=\"today 5-y\", geo=\"\", grop=\"\"):\n        self.keywords = keywords\n        self.sanitized_keywords = [\n            keyword.replace(\" \", \"+\")\n            for keyword in keywords\n        ]\n        self.category = category\n        self.time_frame = time_frame\n        self.geo = geo\n        self.grop = grop\n        self.refresh_time = refresh_time\n        self.next_refresh = time.time()\n\n    def __str__(self):\n        return f\"{self.keywords} {self.time_frame}\"\n\n\nclass GoogleServiceFeed(service_feeds.AbstractServiceFeed):\n    FEED_CHANNEL = GoogleServiceFeedChannel\n    REQUIRED_SERVICES = [Services_bases.GoogleService]\n\n    def __init__(self, config, main_async_loop, bot_id):\n        super().__init__(config, main_async_loop, bot_id)\n        self.trends_req_builder = None\n        self.trends_topics = []\n\n    def _initialize(self):\n        # if the url changes (google sometimes changes it), use the following line:\n        # trends_req.GENERAL_URL = \"https://trends.google.com/trends/explore\"\n        self.trends_req_builder = simplifiedpytrends.request.TrendReq(hl='en-US', tz=0)\n\n    # merge new config into existing config\n    def update_feed_config(self, config):\n        self.trends_topics.extend(topic\n                                  for topic in config[services_constants.CONFIG_TREND_TOPICS]\n                                  if topic not in self.trends_topics)\n\n    def _something_to_watch(self):\n        return bool(self.trends_topics)\n\n    def _get_sleep_time_before_next_wakeup(self):\n        closest_wakeup = min(topic.next_refresh for topic in self.trends_topics)\n        return max(0, closest_wakeup - time.time())\n\n    async def _get_topic_trend(self, topic):\n        self.logger.debug(f\"Fetching trend on {topic.keywords} over {topic.time_frame}\")\n        await self.trends_req_builder.async_build_payload(kw_list=topic.sanitized_keywords,\n                                                          cat=topic.category,\n                                                          timeframe=topic.time_frame,\n                                                          geo=topic.geo,\n                                                          gprop=topic.grop)\n        topic.next_refresh = time.time() + topic.refresh_time\n        return await self.trends_req_builder.async_interest_over_time()\n\n    async def _push_update_and_wait(self):\n        for topic in self.trends_topics:\n            if time.time() >= topic.next_refresh:\n                interest_over_time = await self._get_topic_trend(topic)\n                if interest_over_time:\n                    await self._async_notify_consumers(\n                        {\n                            services_constants.FEED_METADATA: f\"{topic};{interest_over_time}\",\n                            services_constants.CONFIG_TREND: interest_over_time,\n                            services_constants.CONFIG_TREND_DESCRIPTION: topic\n                        }\n                    )\n        await asyncio.sleep(self._get_sleep_time_before_next_wakeup())\n\n    async def _update_loop(self):\n        async with aiohttp.ClientSession() as session:\n            self.trends_req_builder.aiohttp_session = session\n            while not self.should_stop:\n                try:\n                    await self._push_update_and_wait()\n                except simplifiedpytrends.exceptions.ResponseError as e:\n                    self.logger.exception(e, True, f\"Error when fetching Google trends feed: {e} \"\n                                                   f\"(response text: {await e.response.text()})\")\n                    self.should_stop = True\n                except Exception as e:\n                    self.logger.exception(e, True, f\"Error when receiving Google feed: ({e})\")\n                    self.should_stop = True\n            return False\n\n    async def _start_service_feed(self):\n        try:\n            asyncio.create_task(self._update_loop())\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when initializing Google trends feed: {e}\")\n            return False\n        return True\n"
  },
  {
    "path": "Services/Services_feeds/google_service_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GoogleServiceFeed\"],\n  \"tentacles-requirements\": [\"google_service\"]\n}"
  },
  {
    "path": "Services/Services_feeds/reddit_service_feed/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .reddit_feed import RedditServiceFeed\n"
  },
  {
    "path": "Services/Services_feeds/reddit_service_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"RedditServiceFeed\"],\n  \"tentacles-requirements\": [\"reddit_service\"]\n}"
  },
  {
    "path": "Services/Services_feeds/reddit_service_feed/reddit_feed.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport time\nimport asyncprawcore.exceptions\nimport logging\n\nimport octobot_commons.constants as commons_constants\nimport octobot_services.channel as services_channel\nimport octobot_services.constants as services_constants\nimport octobot_services.service_feeds as service_feeds\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass RedditServiceFeedChannel(services_channel.AbstractServiceFeedChannel):\n    pass\n\n\nclass RedditServiceFeed(service_feeds.AbstractServiceFeed):\n    FEED_CHANNEL = RedditServiceFeedChannel\n    REQUIRED_SERVICES = [Services_bases.RedditService]\n\n    MAX_CONNECTION_ATTEMPTS = 10\n\n    def __init__(self, config, main_async_loop, bot_id):\n        service_feeds.AbstractServiceFeed.__init__(self, config, main_async_loop, bot_id)\n        self.subreddits = None\n        self.counter = 0\n        self.connect_attempts = 0\n        self.credentials_ok = False\n        self.listener_task = None\n\n    # merge new config into existing config\n    def update_feed_config(self, config):\n        if services_constants.CONFIG_REDDIT_SUBREDDITS in self.feed_config:\n            self.feed_config[services_constants.CONFIG_REDDIT_SUBREDDITS] = {\n                **self.feed_config[services_constants.CONFIG_REDDIT_SUBREDDITS],\n                **config[services_constants.CONFIG_REDDIT_SUBREDDITS]}\n        else:\n            self.feed_config[services_constants.CONFIG_REDDIT_SUBREDDITS] = config[\n                services_constants.CONFIG_REDDIT_SUBREDDITS]\n\n    def _init_subreddits(self):\n        self.subreddits = \"\"\n        for symbol in self.feed_config[services_constants.CONFIG_REDDIT_SUBREDDITS]:\n            for subreddit in self.feed_config[services_constants.CONFIG_REDDIT_SUBREDDITS][symbol]:\n                if subreddit not in self.subreddits:\n                    if self.subreddits:\n                        self.subreddits = self.subreddits + \"+\" + subreddit\n                    else:\n                        self.subreddits = self.subreddits + subreddit\n\n    def _initialize(self):\n        if not self.subreddits:\n            self._init_subreddits()\n\n    def _something_to_watch(self):\n        return services_constants.CONFIG_REDDIT_SUBREDDITS in self.feed_config and self.feed_config[\n            services_constants.CONFIG_REDDIT_SUBREDDITS]\n\n    @staticmethod\n    def _get_entry_weight(entry_age):\n        if entry_age > 0:\n            # entry in history => weight proportional to entry's age\n            # last 12 hours: weight = 4\n            # last 2 days: weight = 3\n            # last 7 days: weight = 2\n            # older: weight = 1\n            if entry_age / commons_constants.HOURS_TO_SECONDS <= 12:\n                return 4\n            elif entry_age / commons_constants.DAYS_TO_SECONDS <= 2:\n                return 3\n            elif entry_age / commons_constants.DAYS_TO_SECONDS <= 7:\n                return 2\n            else:\n                return 1\n        # new entry => max weight\n        return 5\n\n    async def _start_listener(self):\n        # avoid debug log at each asyncprawcore fetch\n        logging.getLogger(\"asyncprawcore\").setLevel(logging.WARNING)\n        subreddit = await self.services[0].get_endpoint().subreddit(self.subreddits)\n        start_time = time.time()\n        async for entry in subreddit.stream.submissions():\n            self.credentials_ok = True\n            self.connect_attempts = 0\n            self.counter += 1\n            # check if we are in the 100 history or if it's a new entry (new posts are more valuables)\n            # the older the entry is, the les weight it gets\n            entry_age_when_feed_started_in_sec = start_time - entry.created_utc\n            entry_weight = self._get_entry_weight(entry_age_when_feed_started_in_sec)\n            await self._async_notify_consumers(\n                {\n                    services_constants.FEED_METADATA: entry.subreddit.display_name.lower(),\n                    services_constants.CONFIG_REDDIT_ENTRY: entry,\n                    services_constants.CONFIG_REDDIT_ENTRY_WEIGHT: entry_weight\n                }\n            )\n\n    async def _start_listener_task(self):\n        while not self.should_stop and self.connect_attempts < self.MAX_CONNECTION_ATTEMPTS:\n            try:\n                await self._start_listener()\n            except asyncprawcore.exceptions.RequestException:\n                # probably a connexion loss, try again\n                time.sleep(self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC)\n            except asyncprawcore.exceptions.InvalidToken as e:\n                # expired, try again\n                self.logger.exception(e, True, f\"Error when receiving Reddit feed: '{e}'\")\n                self.logger.info(f\"Try to continue after {self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC} seconds.\")\n                time.sleep(self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC)\n            except asyncprawcore.exceptions.ServerError as e:\n                # server error, try again\n                self.logger.exception(e, True, \"Error when receiving Reddit feed: '{e}'\")\n                self.logger.info(f\"Try to continue after {self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC} seconds.\")\n                time.sleep(self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC)\n            except asyncprawcore.exceptions.OAuthException as e:\n                self.logger.exception(e, True, f\"Error when receiving Reddit feed: '{e}' this may mean that reddit \"\n                                               f\"login info in config.json are wrong\")\n                self.keep_running = False\n                self.should_stop = True\n            except asyncprawcore.exceptions.ResponseException as e:\n                message_complement = \"this may mean that reddit login info in config.json are invalid.\" \\\n                    if not self.credentials_ok else \\\n                    f\"Try to continue after {self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC} seconds.\"\n                self.logger.exception(e, True,\n                                      f\"Error when receiving Reddit feed: '{e}' this may mean {message_complement}\")\n                if not self.credentials_ok:\n                    self.connect_attempts += 1\n                else:\n                    self.connect_attempts += 0.1\n                time.sleep(self._SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC)\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when receiving Reddit feed: '{e}'\")\n                self.keep_running = False\n                self.should_stop = True\n        return False\n\n    async def _start_service_feed(self):\n        self.listener_task = asyncio.create_task(self._start_listener_task())\n        return True\n\n    async def stop(self):\n        await super().stop()\n        if self.listener_task is not None:\n            self.listener_task.cancel()\n            self.listener_task = None\n"
  },
  {
    "path": "Services/Services_feeds/telegram_api_service_feed/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .telegram_api_feed import TelegramApiServiceFeed\n"
  },
  {
    "path": "Services/Services_feeds/telegram_api_service_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramApiServiceFeed\"],\n  \"tentacles-requirements\": [\"telegram_api_service\"]\n}"
  },
  {
    "path": "Services/Services_feeds/telegram_api_service_feed/telegram_api_feed.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport telethon\n\nimport octobot_services.channel as services_channel\nimport octobot_services.constants as services_constants\nimport octobot_services.service_feeds as service_feeds\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass TelegramApiServiceFeedChannel(services_channel.AbstractServiceFeedChannel):\n    pass\n\n\nclass TelegramApiServiceFeed(service_feeds.AbstractServiceFeed):\n    FEED_CHANNEL = TelegramApiServiceFeedChannel\n    REQUIRED_SERVICES = [Services_bases.TelegramApiService]\n\n    def __init__(self, config, main_async_loop, bot_id):\n        super().__init__(config, main_async_loop, bot_id)\n        self.feed_config = {\n            services_constants.CONFIG_TELEGRAM_ALL_CHANNEL: True,\n        }\n\n    def update_feed_config(self, config):\n        pass\n\n    def _add_event_handler(self):\n        self.services[0].add_event_handler(self.message_handler, telethon.events.NewMessage)\n\n    async def message_handler(self, event):\n        try:\n            display_name = self.get_display_name(await event.get_sender())\n            if self.feed_config[services_constants.CONFIG_TELEGRAM_ALL_CHANNEL]:\n                media_output_path = None\n                if event.message.media is not None:\n                    media_output_path = await self.services[0].download_media_from_message(message=event.message,\n                                                                                           source=display_name)\n                await self.feed_send_coroutine(\n                    {\n                        services_constants.CONFIG_MESSAGE_SENDER: display_name,\n                        services_constants.CONFIG_MESSAGE_CONTENT: event.text,\n                        services_constants.CONFIG_IS_GROUP_MESSAGE: event.is_group,\n                        services_constants.CONFIG_IS_CHANNEL_MESSAGE: event.is_channel,\n                        services_constants.CONFIG_IS_PRIVATE_MESSAGE: event.is_private,\n                        services_constants.CONFIG_MEDIA_PATH: media_output_path,\n                    }\n                )\n            else:\n                self.logger.debug(f\"Ignored message from {display_name}: not in followed telegram users \"\n                                  f\"(message: {event.text})\")\n        except Exception as e:\n            self.logger.error(f\"Fail to parse incoming message : {e}\")\n\n    def get_display_name(self, entity):\n        if isinstance(entity, telethon.types.User):\n            if entity.last_name and entity.first_name:\n                return f\"{entity.first_name} {entity.last_name}\"\n            elif entity.first_name:\n                return entity.first_name\n            elif entity.last_name:\n                return entity.last_name\n            else:\n                return \"\"\n        elif isinstance(entity, (telethon.types.Chat, telethon.types.ChatForbidden, telethon.types.Channel)):\n            return entity.title\n        return \"\"\n\n    def _something_to_watch(self):\n        return self.feed_config[services_constants.CONFIG_TELEGRAM_ALL_CHANNEL]\n\n    @staticmethod\n    def _get_service_layer_service_feed():\n        return Services_bases.TelegramApiService\n\n    def _initialize(self):\n        self._add_event_handler()\n\n    async def _start_service_feed(self):\n        return True\n"
  },
  {
    "path": "Services/Services_feeds/telegram_service_feed/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .telegram_feed import TelegramServiceFeed\n"
  },
  {
    "path": "Services/Services_feeds/telegram_service_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TelegramServiceFeed\"],\n  \"tentacles-requirements\": [\"telegram_service\"]\n}"
  },
  {
    "path": "Services/Services_feeds/telegram_service_feed/telegram_feed.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport telegram\nimport telegram.ext\n\nimport octobot_services.channel as services_channel\nimport octobot_services.constants as services_constants\nimport octobot_services.service_feeds as service_feeds\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass TelegramServiceFeedChannel(services_channel.AbstractServiceFeedChannel):\n    pass\n\n\nclass TelegramServiceFeed(service_feeds.AbstractServiceFeed):\n    FEED_CHANNEL = TelegramServiceFeedChannel\n    REQUIRED_SERVICES = [Services_bases.TelegramService]\n\n    HANDLED_CHATS = [telegram.constants.ChatType.GROUP, telegram.constants.ChatType.CHANNEL]\n\n    def __init__(self, config, main_async_loop, bot_id):\n        super().__init__(config, main_async_loop, bot_id)\n        self.feed_config = {\n            services_constants.CONFIG_TELEGRAM_ALL_CHANNEL: False,\n            services_constants.CONFIG_TELEGRAM_CHANNEL: []\n        }\n\n    # configure the whitelist of Telegram groups/channels to listen to\n    # merge new config into existing config\n    def update_feed_config(self, config):\n        self.feed_config[services_constants.CONFIG_TELEGRAM_CHANNEL].extend(\n            channel for channel in config[services_constants.CONFIG_TELEGRAM_CHANNEL]\n            if channel not in\n            self.feed_config[services_constants.CONFIG_TELEGRAM_CHANNEL]\n        )\n\n    # if True, disable channel whitelist and listen to every group/channel it is invited to\n    def set_listen_to_all_groups_and_channels(self, activate=True):\n        self.feed_config[services_constants.CONFIG_TELEGRAM_ALL_CHANNEL] = activate\n\n    def _register_to_service(self):\n        if not self.services[0].is_registered(self.get_name()):\n            self.services[0].register_user(self.get_name())\n            self.services[0].register_text_polling_handler(self.HANDLED_CHATS, self._feed_callback)\n\n    async def _feed_callback(self, update: telegram.Update, _: telegram.ext.ContextTypes.DEFAULT_TYPE):\n        message = update.effective_message.text\n        chat = update.effective_chat.title\n        if (\n            self.feed_config[services_constants.CONFIG_TELEGRAM_ALL_CHANNEL]\n            or chat in self.feed_config[services_constants.CONFIG_TELEGRAM_CHANNEL]\n        ):\n            message_desc = str(update)\n            await self._async_notify_consumers(\n                {\n                    services_constants.FEED_METADATA: message_desc,\n                    services_constants.CONFIG_GROUP_MESSAGE: update,\n                    services_constants.CONFIG_GROUP_MESSAGE_DESCRIPTION: message.lower()\n                }\n            )\n        else:\n            self.logger.debug(f\"Ignored message from {chat} chat: not in followed telegram chats (message: {message})\")\n\n    def _something_to_watch(self):\n        return self.feed_config[services_constants.CONFIG_TELEGRAM_ALL_CHANNEL] or self.feed_config[\n            services_constants.CONFIG_TELEGRAM_CHANNEL]\n\n    @staticmethod\n    def _get_service_layer_service_feed():\n        return Services_bases.TelegramService\n\n    def _initialize(self):\n        self._register_to_service()\n\n    async def _start_service_feed(self):\n        return True\n"
  },
  {
    "path": "Services/Services_feeds/trading_view_service_feed/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .trading_view_feed import TradingViewServiceFeed\n"
  },
  {
    "path": "Services/Services_feeds/trading_view_service_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TradingViewServiceFeed\"],\n  \"tentacles-requirements\": [\"trading_view_service\"]\n}"
  },
  {
    "path": "Services/Services_feeds/trading_view_service_feed/trading_view_feed.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_services.channel as services_channel\nimport octobot_services.constants as services_constants\nimport octobot_services.service_feeds as service_feeds\nimport octobot_commons.authentication as authentication\nimport tentacles.Services.Services_bases as Services_bases\n\n\nclass TradingViewServiceFeedChannel(services_channel.AbstractServiceFeedChannel):\n    pass\n\n\nclass TradingViewServiceFeed(service_feeds.AbstractServiceFeed):\n    FEED_CHANNEL = TradingViewServiceFeedChannel\n    REQUIRED_SERVICES = [Services_bases.WebHookService, Services_bases.TradingViewService]\n\n    def __init__(self, config, main_async_loop, bot_id):\n        super().__init__(config, main_async_loop, bot_id)\n        self.webhook_service_name = services_constants.TRADINGVIEW_WEBHOOK_SERVICE_NAME\n        self.webhook_service_url = \"\"\n\n    def _something_to_watch(self):\n        return bool(self.channel.consumers)\n\n    def ensure_callback_auth(self, data) -> bool:\n        if self.services[1].requires_token:\n            split_result = data.split(\"TOKEN=\")\n            if len(split_result) > 1:\n                token = split_result[1].strip().split(\"\\n\")[0]\n                return self.services[1].token == token\n            return False\n        # no token expected\n        return True\n\n    def webhook_callback(self, data):\n        self.logger.info(f\"Received : {data}\")\n        self._notify_consumers(\n            {\n                services_constants.FEED_METADATA: data,\n            }\n        )\n\n    async def async_webhook_callback(self, data):\n        self.logger.info(f\"Received : {data}\")\n        await self._async_notify_consumers(\n            {\n                services_constants.FEED_METADATA: data,\n            }\n        )\n\n    def _register_to_service(self):\n        service = self.services[0]\n        if not service.is_subscribed(self.webhook_service_name):\n            callback = self.async_webhook_callback if service.is_using_cloud_webhooks() else self.webhook_callback\n            service.subscribe_feed(\n                self.webhook_service_name, callback, self.ensure_callback_auth\n            )\n\n    def _initialize(self):\n        self._register_to_service()\n\n    async def _start_service_feed(self):\n        success = await self.services[0].start_webhooks()\n        self.webhook_service_url = self.services[0].get_subscribe_url(self.webhook_service_name)\n        if success:\n            self.services[1].register_webhook_url(self.webhook_service_url)\n            address_details = (\n                f\"email address is: {authentication.Authenticator.instance().get_saved_tradingview_email()}\"\n                if self.services[0].use_octobot_cloud_email_webhook\n                else f\"webhook url is: {self.webhook_service_url}\"\n            )\n            self.logger.info(f\"Your OctoBot's TradingView {address_details}    \"\n                             f\"the pin code for this alert is: {self.services[1].token}\")\n        return success\n"
  },
  {
    "path": "Services/Services_feeds/twitter_service_feed/__init__.py",
    "content": "import octobot_commons.constants as commons_constants\nif not commons_constants.USE_MINIMAL_LIBS:\n    from .twitter_feed import TwitterServiceFeed\n"
  },
  {
    "path": "Services/Services_feeds/twitter_service_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TwitterServiceFeed\"],\n  \"tentacles-requirements\": [\"twitter_service\"]\n}"
  },
  {
    "path": "Services/Services_feeds/twitter_service_feed/twitter_feed.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport threading\n# comment imports to remove twitter from dependencies when tentacle is disabled\n# import twitter\n\nimport octobot_services.channel as services_channel\nimport octobot_services.constants as services_constants\nimport octobot_services.service_feeds as service_feeds\nimport tentacles.Services.Services_bases as Services_bases\n\n\n# disable inheritance to disable tentacle visibility. Disabled as starting from feb 9 2023, API is now paid only\n# class TwitterServiceFeedChannel(services_channel.AbstractServiceFeedChannel):\nclass TwitterServiceFeedChannel:\n    pass\n\n\n# disable inheritance to disable tentacle visibility. Disabled as starting from feb 9 2023, API is now paid only\n# class TwitterServiceFeed(service_feeds.AbstractServiceFeed, threading.Thread):\nclass TwitterServiceFeed:\n    FEED_CHANNEL = TwitterServiceFeedChannel\n    REQUIRED_SERVICES = [Services_bases.TwitterService]\n\n    def __init__(self, config, main_async_loop, bot_id):\n        super().__init__(config, main_async_loop, bot_id)\n        threading.Thread.__init__(self, name=self.get_name())\n        self.user_ids = []\n        self.hashtags = []\n        self.counter = 0\n\n    async def _inner_start(self) -> bool:\n        threading.Thread.start(self)\n        return True\n\n    # merge new config into existing config\n    def update_feed_config(self, config):\n        if services_constants.CONFIG_TWITTERS_ACCOUNTS in self.feed_config:\n            self.feed_config[services_constants.CONFIG_TWITTERS_ACCOUNTS] = {\n                **self.feed_config[services_constants.CONFIG_TWITTERS_ACCOUNTS],\n                **config[services_constants.CONFIG_TWITTERS_ACCOUNTS]}\n        else:\n            self.feed_config[services_constants.CONFIG_TWITTERS_ACCOUNTS] = config[\n                services_constants.CONFIG_TWITTERS_ACCOUNTS]\n\n        if services_constants.CONFIG_TWITTERS_HASHTAGS in self.feed_config:\n            self.feed_config[services_constants.CONFIG_TWITTERS_HASHTAGS] = {\n                **self.feed_config[services_constants.CONFIG_TWITTERS_HASHTAGS],\n                **config[services_constants.CONFIG_TWITTERS_HASHTAGS]}\n        else:\n            self.feed_config[services_constants.CONFIG_TWITTERS_HASHTAGS] = config[\n                services_constants.CONFIG_TWITTERS_HASHTAGS]\n\n    def _init_users_accounts(self):\n        tempo_added_accounts = []\n        for symbol in self.feed_config[services_constants.CONFIG_TWITTERS_ACCOUNTS]:\n            for account in self.feed_config[services_constants.CONFIG_TWITTERS_ACCOUNTS][symbol]:\n                if account not in tempo_added_accounts:\n                    tempo_added_accounts.append(account)\n                    try:\n                        self.user_ids.append(str(self.services[0].get_user_id(account)))\n                    except twitter.TwitterError as e:\n                        self.logger.error(account + \" : \" + str(e))\n\n    def _init_hashtags(self):\n        for symbol in self.feed_config[services_constants.CONFIG_TWITTERS_HASHTAGS]:\n            for hashtag in self.feed_config[services_constants.CONFIG_TWITTERS_HASHTAGS][symbol]:\n                if hashtag not in self.hashtags:\n                    self.hashtags.append(hashtag)\n\n    def _initialize(self):\n        if not self.user_ids:\n            self._init_users_accounts()\n        if not self.hashtags:\n            self._init_hashtags()\n\n    def _something_to_watch(self):\n        return (services_constants.CONFIG_TWITTERS_HASHTAGS in self.feed_config and self.feed_config[\n            services_constants.CONFIG_TWITTERS_HASHTAGS]) \\\n               or (services_constants.CONFIG_TWITTERS_ACCOUNTS in self.feed_config and self.feed_config[\n            services_constants.CONFIG_TWITTERS_ACCOUNTS])\n\n    async def _start_listener(self):\n        for tweet in self.services[0].get_endpoint().GetStreamFilter(follow=self.user_ids,\n                                                                     track=self.hashtags,\n                                                                     stall_warnings=True):\n            self.counter += 1\n            string_tweet = self.services[0].get_tweet_text(tweet)\n            if string_tweet:\n                tweet_desc = str(tweet).lower()\n                self._notify_consumers(\n                    {\n                        services_constants.FEED_METADATA: tweet_desc,\n                        services_constants.CONFIG_TWEET: tweet,\n                        services_constants.CONFIG_TWEET_DESCRIPTION: string_tweet.lower()\n                    }\n                )\n\n    async def _start_service_feed(self):\n        while not self.should_stop:\n            try:\n                await self._start_listener()\n            except twitter.error.TwitterError as e:\n                self.logger.exception(e, True, f\"Error when receiving Twitter feed: {e.message} ({e})\")\n                self.should_stop = True\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error when receiving Twitter feed: ({e})\")\n                self.should_stop = True\n        return False\n"
  },
  {
    "path": "Trading/Exchange/ascendex/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Private-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .ascendex_exchange import *\n"
  },
  {
    "path": "Trading/Exchange/ascendex/ascendex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\nimport decimal\n\nimport octobot_commons.enums\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\n\n\nclass AscendEx(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    # text content of errors due to unhandled IP white list issues\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # ascendex {\"code\":200001,\"message\":\"You have setup IP allowed list for this key. Your IP address () is not\n        # in the allowed list.\",\"reason\":\"AUTHENTICATION_FAILED\"}\n        (\"ip allowed list\", \"not in the allowed list\"),\n    ]\n\n    BUY_STR = \"Buy\"\n    SELL_STR = \"Sell\"\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n\n    FIX_MARKET_STATUS = True\n\n    ACCOUNTS = {\n        trading_enums.AccountTypes.CASH: 'cash',\n        trading_enums.AccountTypes.MARGIN: 'margin',\n        trading_enums.AccountTypes.FUTURE: 'futures',  # currently in beta\n    }\n\n    @classmethod\n    def get_name(cls):\n        return 'ascendex'\n\n    def get_adapter_class(self):\n        return AscendexCCXTAdapter\n\n    async def switch_to_account(self, account_type):\n        # TODO\n        pass\n\n    def parse_account(self, account):\n        return trading_enums.AccountTypes[account.lower()]\n\n    async def get_my_recent_trades(self, symbol=None, since=None, limit=None, **kwargs):\n        # On AscendEx, account recent trades is available under fetch_closed_orders\n        return await super().get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs)\n\n    async def get_symbol_prices(self,\n                                symbol: str,\n                                time_frame: octobot_commons.enums.TimeFrames,\n                                limit: int = None,\n                                **kwargs: dict) -> typing.Optional[list]:\n        if limit is None:\n            # force default limit on AscendEx since it's not used by default in fetch_ohlcv\n            options = self.connector.client.safe_value(self.connector.client.options, 'fetchOHLCV', {})\n            limit = self.connector.client.safe_integer(options, 'limit', 500)\n        return await super().get_symbol_prices(symbol, time_frame, limit, **kwargs)\n\n\nclass AscendexCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/ascendex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"AscendEx\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/ascendex_websocket_feed/__init__.py",
    "content": "from .ascendex_websocket import AscendexCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/ascendex_websocket_feed/ascendex_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.ascendex.ascendex_exchange as ascendex_exchange\n\n\nclass AscendexCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: Feeds.UNSUPPORTED.value,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return ascendex_exchange.AscendEx.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return ascendex_exchange.AscendexCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/ascendex_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"AscendexCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/ascendex_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/ascendex_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...ascendex_websocket_feed import AscendexCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    async with websocket_test_tools.ws_exchange_manager(config, AscendexCryptofeedWebsocketConnector.get_name()) \\\n            as exchange_manager_instance:\n        await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n            websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n            websocket_connector_class=AscendexCryptofeedWebsocketConnector,\n            exchange_manager=exchange_manager_instance,\n            config=config,\n            symbols=[\"BTC/USDT\", \"ETH/USDT\"],\n            time_frames=[commons_enums.TimeFrames.ONE_HOUR],\n            expected_pushed_channels={\n                channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value,\n            },\n            time_before_assert=20\n        )\n"
  },
  {
    "path": "Trading/Exchange/binance/__init__.py",
    "content": "from .binance_exchange import Binance"
  },
  {
    "path": "Trading/Exchange/binance/binance_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport typing\nimport enum\n\nimport ccxt\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as symbols\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.errors as errors\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nimport octobot_trading.util as trading_util\nimport octobot_trading.personal_data as personal_data\n\n\nclass BinanceMarkets(enum.Enum):\n    SPOT = \"spot\"\n    LINEAR = \"linear\"\n    INVERSE = \"inverse\"\n\n\nclass Binance(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    FIX_MARKET_STATUS = True\n\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    # should be fetched using recent trades.\n    SUPPORTS_SET_MARGIN_TYPE_ON_OPEN_POSITIONS = False  # set False when the exchange refuses to change margin type\n    # when an associated position is open\n    # binance {\"code\":-4048,\"msg\":\"Margin type cannot be changed if there exists position.\"}\n    # Set True when the \"limit\" param when fetching order books is taken into account\n    SUPPORTS_CUSTOM_LIMIT_ORDER_BOOK_FETCH = True\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on futures\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            # not supported or need custom mechanics with batch orders\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on spot\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n\n    # text content of errors due to orders not found errors\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # Binance ex: DDoSProtection('binance {\"code\":-2015,\"msg\":\"Invalid API-key, IP, or permissions for action.\"}')\n        (\"key\", \"permissions for action\"),\n    ]\n    # text content of errors due to traded assets for account\n    EXCHANGE_ACCOUNT_TRADED_SYMBOL_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # Binance ex: InvalidOrder binance {\"code\":-2010,\"msg\":\"This symbol is not permitted for this account.\"}\n        (\"symbol\", \"not permitted\", \"for this account\"),\n        # ccxt.base.errors.InvalidOrder: binance {\"code\":-2010,\"msg\":\"Symbol not whitelisted for API key.\"}\n        (\"symbol\", \"not whitelisted\"),\n    ]\n    # text content of errors due to a closed position on the exchange. Relevant for reduce-only orders\n    EXCHANGE_CLOSED_POSITION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # doesn't seem to happen on binance\n    ]\n    # text content of errors due to an order that would immediately trigger if created. Relevant for stop losses\n    EXCHANGE_ORDER_IMMEDIATELY_TRIGGER_ERRORS: typing.List[typing.Iterable[str]] = [\n        # binance {\"code\":-2021,\"msg\":\"Order would immediately trigger.\"}\n        (\"order would immediately trigger\", )\n    ]\n    # text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)\n    EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [\n        ('Unknown order sent', )\n    ]\n    # set when the exchange can allow users to pay fees in a custom currency (ex: BNB on binance)\n    LOCAL_FEES_CURRENCIES: typing.List[str] = [\"BNB\"]\n\n    # Name of the price param to give ccxt to edit a stop loss\n    STOP_LOSS_EDIT_PRICE_PARAM = ccxt_enums.ExchangeOrderCCXTUnifiedParams.STOP_PRICE.value\n\n    BUY_STR = \"BUY\"\n    SELL_STR = \"SELL\"\n    INVERSE_TYPE = \"inverse\"\n    LINEAR_TYPE = \"linear\"\n\n    def __init__(\n        self, config, exchange_manager, exchange_config_by_exchange: typing.Optional[dict[str, dict]],\n        connector_class=None\n    ):\n        self._futures_account_types = self._infer_account_types(exchange_manager)\n        super().__init__(config, exchange_manager, exchange_config_by_exchange, connector_class=connector_class)\n\n    @classmethod\n    def get_name(cls):\n        return 'binance'\n\n    def get_adapter_class(self):\n        return BinanceCCXTAdapter\n\n    @staticmethod\n    def get_default_reference_market(exchange_name: str) -> str:\n        return \"USDC\"\n\n    def supports_native_edit_order(self, order_type: trading_enums.TraderOrderType) -> bool:\n        # return False when default edit_order can't be used and order should always be canceled and recreated instead\n        is_stop = order_type in (\n            trading_enums.TraderOrderType.STOP_LOSS, trading_enums.TraderOrderType.STOP_LOSS_LIMIT\n        )\n        if self.exchange_manager.is_future:\n            # replace not supported in futures stop orders\n            return not is_stop\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        try:\n            with self.connector.error_describer():\n                if self.exchange_manager.is_future:\n                    raw_binance_balance = await self.connector.client.fapiPrivateV3GetBalance()\n                    # accountAlias = unique account code\n                    # from https://binance-docs.github.io/apidocs/futures/en/#futures-account-balance-v3-user_data\n                    return raw_binance_balance[0][\"accountAlias\"]\n                else:\n                    raw_balance = await self.connector.client.fetch_balance()\n                    return raw_balance[ccxt_constants.CCXT_INFO][\"uid\"]\n        except (KeyError, IndexError):\n            # should not happen\n            raise\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        \"\"\"\n        from:\n            https://developers.binance.com/docs/derivatives/usds-margined-futures/common-definition#max_num_orders\n            https://developers.binance.com/docs/binance-spot-api-docs/filters#max_num_orders\n        [\n            {\"filterType\": \"PRICE_FILTER\", \"maxPrice\": \"1000000.00000000\", \"minPrice\": \"0.01000000\", \"tickSize\": \"0.01000000\"}, \n            {\"filterType\": \"LOT_SIZE\", \"maxQty\": \"9000.00000000\", \"minQty\": \"0.00001000\", \"stepSize\": \"0.00001000\"}, \n            {\"filterType\": \"ICEBERG_PARTS\", \"limit\": \"10\"}, \n            {\"filterType\": \"MARKET_LOT_SIZE\", \"maxQty\": \"115.46151096\", \"minQty\": \"0.00000000\", \"stepSize\": \"0.00000000\"}, \n            {\"filterType\": \"TRAILING_DELTA\", \"maxTrailingAboveDelta\": \"2000\", \"maxTrailingBelowDelta\": \"2000\", \"minTrailingAboveDelta\": \"10\", \"minTrailingBelowDelta\": \"10\"}, \n            {\"askMultiplierDown\": \"0.2\", \"askMultiplierUp\": \"5\", \"avgPriceMins\": \"5\", \"bidMultiplierDown\": \"0.2\", \"bidMultiplierUp\": \"5\", \"filterType\": \"PERCENT_PRICE_BY_SIDE\"}, \n            {\"applyMaxToMarket\": False, \"applyMinToMarket\": True, \"avgPriceMins\": \"5\", \"filterType\": \"NOTIONAL\", \"maxNotional\": \"9000000.00000000\", \"minNotional\": \"5.00000000\"}, \n            {\"filterType\": \"MAX_NUM_ORDERS\", \"maxNumOrders\": \"200\"}, \n            {\"filterType\": \"MAX_NUM_ALGO_ORDERS\", \"maxNumAlgoOrders\": \"5\"}\n        ]\n        => usually:\n            - SPOT: MAX_NUM_ORDERS 200 MAX_NUM_ALGO_ORDERS 5\n            - FUTURES: MAX_NUM_ORDERS 200 MAX_NUM_ALGO_ORDERS 10\n        \"\"\"\n        try:\n            market_status = self.get_market_status(symbol, with_fixer=False)\n            filters = market_status[ccxt_constants.CCXT_INFO][\"filters\"]\n            key = \"MAX_NUM_ALGO_ORDERS\" if personal_data.is_stop_order(order_type) else \"MAX_NUM_ORDERS\"\n            value_key = \"maxNumAlgoOrders\" if personal_data.is_stop_order(order_type) else \"maxNumOrders\"\n            fallback_value_key = \"limit\"    # sometimes, \"limit\" is the key\n            for filter_element in filters:\n                if filter_element.get(\"filterType\") == key:\n                    key = value_key if value_key in filter_element else fallback_value_key\n                    return int(filter_element[key])\n            raise ValueError(f\"{key} not found in filters: {filters}\")\n        except Exception as err:\n            default_count = super().get_max_orders_count(symbol, order_type)\n            self.logger.exception(\n                err, True, f\"Error when computing max orders count: {err}. Using default value: {default_count}\"\n            )\n            return default_count\n\n    def uses_demo_trading_instead_of_sandbox(self) -> bool:\n        if self.exchange_manager.is_future:\n            return True\n        return False\n\n    def _infer_account_types(self, exchange_manager):\n        account_types = []\n        symbol_counts = trading_util.get_symbol_types_counts(exchange_manager.config, True)\n        # only enable the trading type with the majority of asked symbols\n        # todo remove this and use both types when exchange-side multi portfolio is enabled\n        linear_count = symbol_counts.get(trading_enums.FutureContractType.LINEAR_PERPETUAL.value, 0)\n        inverse_count = symbol_counts.get(trading_enums.FutureContractType.INVERSE_PERPETUAL.value, 0)\n        if linear_count >= inverse_count:\n            account_types.append(self.LINEAR_TYPE)   # allows to fetch linear markets\n            if inverse_count:\n                exchange_manager.logger.error(\n                    f\"For now, due to the inverse and linear portfolio split on Binance Futures, OctoBot only \"\n                    f\"supports either linear or inverse trading at a time. Ignoring {inverse_count} inverse \"\n                    f\"futures trading pair as {linear_count} linear futures trading pairs are enabled.\"\n                )\n        else:\n            account_types.append(self.INVERSE_TYPE)  # allows to fetch inverse markets\n            if linear_count:\n                exchange_manager.logger.error(\n                    f\"For now, due to the inverse and linear portfolio split on Binance Futures, OctoBot only \"\n                    f\"supports either linear or inverse trading at a time. Ignoring {linear_count} linear \"\n                    f\"futures trading pair as {inverse_count} inverse futures trading pairs are enabled.\"\n                )\n        return account_types\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_additional_connector_config(self):\n        config = {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"quoteOrderQty\": True,  # enable quote conversion for market orders\n                \"recvWindow\": 60000,    # default is 10000, avoid time related issues\n                \"fetchPositions\": \"account\",    # required to fetch empty positions as well\n                \"filterClosed\": False,  # return empty positions as well\n            }\n        }\n        if self.FETCH_MIN_EXCHANGE_MARKETS:\n            config[ccxt_constants.CCXT_OPTIONS][ccxt_constants.CCXT_FETCH_MARKETS] = (\n                [\n                    BinanceMarkets.LINEAR.value, BinanceMarkets.INVERSE.value\n                ] if self.exchange_manager.is_future else [BinanceMarkets.SPOT.value]\n            )\n        return config\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        signature_identifier = \"signature=\"\n        return bool(\n            (\n                url\n                and signature_identifier in url # for GET & DELETE requests\n            ) or (\n                body\n                and signature_identifier in body # for other requests\n            )\n        )\n\n    async def get_balance(self, **kwargs: dict):\n        if self.exchange_manager.is_future:\n            balance = []\n            for account_type in self._futures_account_types:\n                balance.append(await super().get_balance(**kwargs, subType=account_type))\n            # todo remove this and use both types when exchange-side multi portfolio is enabled\n            # there will only be 1 balance as both linear and inverse are not supported simultaneously\n            # (only 1 _futures_account_types is allowed for now)\n            return balance[0]\n        return await super().get_balance(**kwargs)\n\n    def get_order_additional_params(self, order) -> dict:\n        params = {}\n        if self.exchange_manager.is_future:\n            params[\"reduceOnly\"] = order.reduce_only\n        return params\n\n    def order_request_kwargs_factory(\n        self, \n        exchange_order_id: str, \n        order_type: typing.Optional[trading_enums.TraderOrderType] = None, \n        **kwargs\n    ) -> dict:\n        params = kwargs or {}\n        try:\n            if \"stop\" not in params:\n                order_type = (\n                    order_type or \n                    self.exchange_manager.exchange_personal_data.orders_manager.get_order(\n                        None, exchange_order_id=exchange_order_id\n                    ).order_type\n                )\n                params[\"stop\"] = (\n                    personal_data.is_stop_order(order_type)\n                    or personal_data.is_take_profit_order(order_type)\n                )\n        except KeyError as err:\n            self.logger.warning(\n                f\"Order {exchange_order_id} not found in order manager: considering it a regular (no stop/take profit) order {err}\"\n            )\n        return params\n\n    def fetch_stop_order_in_different_request(self, symbol: str) -> bool:\n        # Override in tentacles when stop orders need to be fetched in a separate request from CCXT\n        # Binance futures uses the algo orders endpoint for stop orders (but not for inverse orders)\n        return self.exchange_manager.is_future and not symbols.parse_symbol(symbol).is_inverse()\n\n    async def _create_market_sell_order(\n        self, symbol, quantity, price=None, reduce_only: bool = False, params=None\n        ) -> dict:\n        # force price to None to avoid selling using quote amount (force market sell quantity in base amount)\n        return await super()._create_market_sell_order(\n            symbol, quantity, price=None, reduce_only=reduce_only, params=params\n        )\n\n    async def set_symbol_partial_take_profit_stop_loss(self, symbol: str, inverse: bool,\n                                                       tp_sl_mode: trading_enums.TakeProfitStopLossMode):\n        \"\"\"\n        take profit / stop loss mode does not exist on binance futures\n        \"\"\"\n\n    async def get_positions(self, symbols=None, **kwargs: dict) -> list:\n        positions = []\n        if \"useV2\" not in kwargs:\n            kwargs[\"useV2\"] = True  #V2 api is required to fetch empty positions (not retured in V3)\n        if \"subType\" in kwargs:\n            return _filter_positions(await super().get_positions(symbols=symbols, **kwargs))\n        for account_type in self._futures_account_types:\n            kwargs[\"subType\"] = account_type\n            positions += await super().get_positions(symbols=symbols, **kwargs)\n        return _filter_positions(positions)\n\n    async def get_position(self, symbol: str, **kwargs: dict) -> dict:\n        # fetchPosition() supports option markets only\n        # => use get_positions\n        return (await self.get_positions(symbols=[symbol], **kwargs))[0]\n\n    async def get_symbol_leverage(self, symbol: str, **kwargs: dict):\n        \"\"\"\n        :param symbol: the symbol\n        :return: the current symbol leverage multiplier\n        \"\"\"\n        # leverage is in position\n        return self.connector.adapter.adapt_leverage(await self.get_position(symbol))\n\n    async def get_all_currencies_price_ticker(self, **kwargs: dict) -> typing.Optional[dict[str, dict]]:\n        if \"subType\" in kwargs or not self.exchange_manager.is_future:\n            return await super().get_all_currencies_price_ticker(**kwargs)\n        # futures with unspecified subType: fetch both linear and inverse tickers\n        linear_tickers = await super().get_all_currencies_price_ticker(subType=self.LINEAR_TYPE, **kwargs)\n        inverse_tickers = await super().get_all_currencies_price_ticker(subType=self.INVERSE_TYPE, **kwargs)\n        return {**linear_tickers, **inverse_tickers}\n\n    async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: dict):\n        \"\"\"\n        Set the symbol margin type\n        :param symbol: the symbol\n        :param isolated: when False, margin type is cross, else it's isolated\n        :return: the update result\n        \"\"\"\n        try:\n            return await super().set_symbol_margin_type(symbol, isolated, **kwargs)\n        except ccxt.ExchangeError as err:\n            raise errors.NotSupported(err) from err\n\n\nclass BinanceCCXTAdapter(exchanges.CCXTAdapter):\n    STOP_ORDERS = [\n        \"stop_market\", \"stop\", # futures\n        \"stop_loss\", \"stop_loss_limit\"  # spot\n    ]\n    TAKE_PROFITS_ORDERS = [\n        \"take_profit_market\", \"take_profit_limit\",    # futures\n        \"take_profit\"  # spot\n    ]\n    BINANCE_DEFAULT_FUNDING_TIME = 8 * commons_constants.HOURS_TO_SECONDS\n\n    def fix_order(self, raw, symbol=None, **kwargs):\n        fixed = super().fix_order(raw, symbol=symbol, **kwargs)\n        self._adapt_order_type(fixed)\n        if fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STATUS.value, None) == \"PENDING_NEW\":\n            # PENDING_NEW order are old orders on binance and should be considered as open\n            fixed[ccxt_enums.ExchangeOrderCCXTColumns.STATUS.value] = trading_enums.OrderStatus.OPEN.value\n        return fixed\n\n    def _adapt_order_type(self, fixed):\n        order_info = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.INFO.value, {})\n        info_order_type = (order_info.get(\"type\", {}) or order_info.get(\"orderType\", None) or \"\").lower()\n        is_stop = info_order_type in self.STOP_ORDERS\n        is_tp = info_order_type in self.TAKE_PROFITS_ORDERS\n        if is_stop or is_tp:\n            if trigger_price := fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TRIGGER_PRICE.value, None):\n                selling = (\n                    fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.SIDE.value, None)\n                    == trading_enums.TradeOrderSide.SELL.value\n                )\n                updated_type = trading_enums.TradeOrderType.UNKNOWN.value\n                trigger_above = False\n                if is_stop:\n                    updated_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                    # force price to trigger price\n                    fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = trigger_price\n                    trigger_above = not selling # sell stop loss triggers when price is lower than target\n                elif is_tp:\n                    # updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value\n                    # take profits are not yet handled as such: consider them as limit orders\n                    updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling\n                    if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:\n                        fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = trigger_price # waiting for TP handling\n                    trigger_above = selling # sell take profit triggers when price is higher than target\n                else:\n                    self.logger.error(\n                        f\"Unknown [{self.connector.exchange_manager.exchange_name}] order type, order: {fixed}\"\n                    )\n                # stop loss and take profits are not tagged as such by ccxt, force it\n                fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type\n                fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above\n            else:\n                self.logger.error(\n                    f\"Unknown [{self.connector.exchange_manager.exchange_name}] order: stop order \"\n                    f\"with no trigger price, order: {fixed}\"\n                )\n        return fixed\n\n    def fix_trades(self, raw, **kwargs):\n        raw = super().fix_trades(raw, **kwargs)\n        for trade in raw:\n            trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value\n        return raw\n\n    def parse_position(self, fixed, force_empty=False, **kwargs):\n        try:\n            return super().parse_position(fixed, force_empty=force_empty, **kwargs)\n        except decimal.InvalidOperation:\n            # on binance, positions might be invalid (ex: LUNAUSD_PERP as None contact size)\n            return None\n\n    def parse_leverage(self, fixed, **kwargs):\n        parsed = super().parse_leverage(fixed, **kwargs)\n        # on binance fixed is a parsed position\n        parsed[trading_enums.ExchangeConstantsLeveragePropertyColumns.LEVERAGE.value] = \\\n            fixed[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value]\n        return parsed\n\n    def parse_funding_rate(self, fixed, from_ticker=False, **kwargs):\n        \"\"\"\n        Binance last funding time is not provided\n        To obtain the last_funding_time :\n        => timestamp(next_funding_time) - timestamp(BINANCE_DEFAULT_FUNDING_TIME)\n        \"\"\"\n        if from_ticker:\n            # no funding info in ticker\n            return {}\n        else:\n            funding_dict = super().parse_funding_rate(fixed, from_ticker=from_ticker, **kwargs)\n            funding_next_timestamp = float(\n                funding_dict.get(trading_enums.ExchangeConstantsFundingColumns.NEXT_FUNDING_TIME.value, 0)\n            )\n            # patch LAST_FUNDING_TIME in tentacle\n            funding_dict.update({\n                trading_enums.ExchangeConstantsFundingColumns.LAST_FUNDING_TIME.value:\n                    max(funding_next_timestamp - self.BINANCE_DEFAULT_FUNDING_TIME, 0)\n            })\n        return funding_dict\n\n\ndef _filter_positions(positions):\n    return [\n        position\n        for position in positions\n        if position is not None\n    ]\n"
  },
  {
    "path": "Trading/Exchange/binance/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Binance\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/binance/resources/binance.md",
    "content": "Binance is a RestExchange adaptation for Binance exchange using the REST API. \n"
  },
  {
    "path": "Trading/Exchange/binance/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/binance/tests/test_sandbox.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport os\n\nimport pytest\n\nimport octobot_commons.tests as commons_tests\nimport octobot_commons.constants as commons_constants\nimport octobot_trading.util.test_tools.spot_rest_exchange_test_tools as spot_rest_exchange_test_tools\nimport octobot_commons.configuration as configuration\nfrom ...binance import Binance\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def _test_spot_rest():\n    config = commons_tests.load_test_config()\n    config[commons_constants.CONFIG_EXCHANGES][Binance.get_name()] = {\n        commons_constants.CONFIG_EXCHANGE_KEY: configuration.encrypt(\n            os.getenv(f\"{Binance.get_name()}_API_KEY\".upper())).decode(),\n        commons_constants.CONFIG_EXCHANGE_SECRET: configuration.encrypt(\n            os.getenv(f\"{Binance.get_name()}_API_SECRET\".upper())).decode()\n    }\n    config[commons_constants.CONFIG_TRADER][commons_constants.CONFIG_ENABLED_OPTION] = True\n    config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_ENABLED_OPTION] = False\n\n    test_tools = spot_rest_exchange_test_tools.SpotRestExchangeTests(config=config, exchange_name=Binance.get_name())\n    test_tools.expected_crypto_in_balance = [\"BNB\", \"BTC\", \"BUSD\", \"ETH\", \"LTC\", \"TRX\", \"USDT\", \"XRP\"]\n    await test_tools.initialize()\n    await test_tools.run(symbol=\"BTC/USDT\")\n    await test_tools.stop()\n    await test_tools.test_all_callback_triggered()\n"
  },
  {
    "path": "Trading/Exchange/binance_websocket_feed/__init__.py",
    "content": "from .binance_websocket import BinanceCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/binance_websocket_feed/binance_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport octobot_commons.constants as commons_constants\nimport tentacles.Trading.Exchange.binance.binance_exchange as binance_exchange\n\n\nclass BinanceCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return binance_exchange.Binance.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return binance_exchange.BinanceCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/binance_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BinanceCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/binance_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/binance_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...binance_websocket_feed import BinanceCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    async with websocket_test_tools.ws_exchange_manager(config, BinanceCryptofeedWebsocketConnector.get_name()) \\\n            as exchange_manager_instance:\n        await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n            websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n            websocket_connector_class=BinanceCryptofeedWebsocketConnector,\n            exchange_manager=exchange_manager_instance,\n            config=config,\n            symbols=[\"BTC/USDT\", \"ETH/BTC\", \"ETH/USDT\"],\n            time_frames=[commons_enums.TimeFrames.ONE_MINUTE,\n                         commons_enums.TimeFrames.ONE_HOUR,\n                         commons_enums.TimeFrames.FOUR_HOURS],\n            expected_pushed_channels={\n                channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,\n                channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,\n                channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value,\n            },\n            time_before_assert=75\n        )\n"
  },
  {
    "path": "Trading/Exchange/binanceus/__init__.py",
    "content": "from .binanceus_exchange import BinanceUS"
  },
  {
    "path": "Trading/Exchange/binanceus/binanceus_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.binance as binance_tentacle\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\n\n\nclass BinanceUS(binance_tentacle.Binance):\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            # not supported or need custom mechanics with batch orders\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,    # unsupported on binance.us, only stop limit orders are supported https://docs.binance.us/#create-new-order-trade\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n\n    @classmethod\n    def get_name(cls):\n        return 'binanceus'\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n        ]\n\n    @staticmethod\n    def get_default_reference_market(exchange_name: str) -> str:\n        return \"USDT\"\n\n    def get_additional_connector_config(self):\n        config = super().get_additional_connector_config()\n        # override to fix ccxt values\n        config[ccxt_constants.CCXT_FEES] = {\n            'trading': {\n                'tierBased': True,\n                'percentage': True,\n                # ccxt replaced values\n                # 'taker': float('0.001'),  # 0.1% trading fee, zero fees for all trading pairs before November 1.\n                # 'maker': float('0.001'),  # 0.1% trading fee, zero fees for all trading pairs before November 1.\n                # 03/03/2025 values https://www.binance.us/fees\n                'taker': float('0.006'),  # 0.600%\n                'maker': float('0.004'),  # 0.400%\n            },\n        }\n        return config\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        # not available on binance.us\n        # see https://docs.binance.us/#get-user-account-information-user_data\n        # vs \"uid\" in regular binance https://binance-docs.github.io/apidocs/spot/en/#spot-account-endpoints\n        return trading_constants.DEFAULT_ACCOUNT_ID\n"
  },
  {
    "path": "Trading/Exchange/binanceus/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BinanceUS\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/binanceus/resources/BinanceUS.md",
    "content": "BinanceUS is a RestExchange adaptation for Binance US exchange using the REST API. \n"
  },
  {
    "path": "Trading/Exchange/binanceus/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/binanceus_websocket_feed/__init__.py",
    "content": "from .binanceus_websocket import BinanceUSCCXTFeedConnector\n"
  },
  {
    "path": "Trading/Exchange/binanceus_websocket_feed/binanceus_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.binanceus.binanceus_exchange as binanceus_exchange\nimport tentacles.Trading.Exchange.binance_websocket_feed.binance_websocket as binance_websocket\n\n\nclass BinanceUSCCXTFeedConnector(binance_websocket.BinanceCCXTWebsocketConnector):\n    @classmethod\n    def get_name(cls):\n        return binanceus_exchange.BinanceUS.get_name()\n"
  },
  {
    "path": "Trading/Exchange/binanceus_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BinanceUSCCXTFeedConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/binanceus_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/binanceus_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...binanceus_websocket_feed import BinanceUSWebsocketFeedConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    async with websocket_test_tools.ws_exchange_manager(config, BinanceUSWebsocketFeedConnector.get_name()) \\\n            as exchange_manager_instance:\n        await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n            websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n            websocket_connector_class=BinanceUSWebsocketFeedConnector,\n            exchange_manager=exchange_manager_instance,\n            config=config,\n            symbols=[\"BTC/USDT\", \"ETH/BTC\", \"ETH/USDT\"],\n            time_frames=[commons_enums.TimeFrames.ONE_HOUR],\n            expected_pushed_channels={\n                channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value\n            },\n            time_before_assert=20\n        )\n"
  },
  {
    "path": "Trading/Exchange/bingx/__init__.py",
    "content": "from .bingx_exchange import Bingx"
  },
  {
    "path": "Trading/Exchange/bingx/bingx_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_connector as ccxt_connector\nimport octobot_trading.enums as trading_enums\n\n\nclass BingxConnector(ccxt_connector.CCXTConnector):\n    def _create_client(self, force_unauth=False):\n        super()._create_client(force_unauth=force_unauth)\n        # bingx v1 spotV1PublicGetMarketKline randomly errors when fetching candles: force V2\n        self.client.spotV1PublicGetMarketKline = self.client.spotV2PublicGetMarketKline\n\nclass Bingx(exchanges.RestExchange):\n    FIX_MARKET_STATUS = True\n    DEFAULT_CONNECTOR_CLASS = BingxConnector    # TODO remove this when ccxt updates to spotV2PublicGetMarketKline\n\n\n    # text content of errors due to orders not found errors\n    EXCHANGE_ORDER_NOT_FOUND_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'bingx {\"code\":100404,\"msg\":\" order not exist\",\"debugMsg\":\"\"}'\n        (\"order not exist\",),\n        # bingx {\"code\":100404,\"msg\":\"the order you want to cancel is FILLED or CANCELLED already, or is not a valid\n        # order id ,please verify\",\"debugMsg\":\"\"}\n        (\"the order you want to cancel is filled or cancelled already\", ),\n        #  bingx {\"code\":100404,\"msg\":\"the order is FILLED or CANCELLED already before, or is not a valid\n        #  order id ,please verify\",\"debugMsg\":\"\"}\n        (\"the order is filled or cancelled already before\", ),\n    ]\n    # text content of errors due to unhandled authentication issues\n    EXCHANGE_AUTHENTICATION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'bingx {'code': '100413', 'msg': 'Incorrect apiKey', 'timestamp': '1725195218082'}'\n        (\"incorrect apikey\",),\n    ]\n    # text content of errors due to api key permissions issues\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'bingx {\"code\":100004,\"msg\":\"Permission denied as the API key was created without the permission，\n        # this api need Spot Trading permission， please config it in https://bingx.com/en/account/api\"'\n        (\"permission denied\", \"trading permission\"),\n    ]\n    # text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)\n    EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [\n        ('the order is filled or cancelled', ''),\n        ('order not exist', '')\n    ]\n    # text content of errors due to unhandled IP white list issues\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # \"PermissionDenied(\"bingx {\"code\":100419,\"msg\":\"your current request IP is xx.xx.xx.xxx does not match IP\n        # whitelist , please go to https://bingx.com/en/account/api/ to verify the ip you have set\",\n        # \"timestamp\":1739291708037}\")\"\n        (\"not match ip whitelist\",),\n    ]\n    \n    # Set True when get_open_order() can return outdated orders (cancelled or not yet created)\n    CAN_HAVE_DELAYED_CANCELLED_ORDERS = True\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            # not supported or need custom mechanics with batch orders\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on spot\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n\n    def get_adapter_class(self):\n        return BingxCCXTAdapter\n\n    @classmethod\n    def get_name(cls) -> str:\n        return 'bingx'\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        with self.connector.error_describer():\n            resp = await self.connector.client.accountV1PrivateGetUid()\n            return resp[\"data\"][\"uid\"]\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        # unknown (05/06/2025)\n        return super().get_max_orders_count(symbol, order_type)\n\n    async def get_my_recent_trades(self, symbol=None, since=None, limit=None, **kwargs):\n        # On SPOT Bingx, account recent trades is available under fetch_closed_orders\n        if self.exchange_manager.is_future:\n            return await super().get_my_recent_trades(symbol=symbol, since=since, limit=limit, **kwargs)\n        return await super().get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs)\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        signature_identifier = \"signature=\"\n        return bool(\n            url\n            and signature_identifier in url\n        )\n\nclass BingxCCXTAdapter(exchanges.CCXTAdapter):\n\n    def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):\n        info = order_or_trade.get(ccxt_constants.CCXT_INFO, {})\n        if stop_price := order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.STOP_LOSS_PRICE.value):\n            # from https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders\n            order_creation_price = float(\n                info.get(\"price\") or order_or_trade.get(\n                    trading_enums.ExchangeConstantsOrderColumns.PRICE.value\n                )\n            )\n            is_selling = (\n                order_or_trade[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]\n                == trading_enums.TradeOrderSide.SELL.value\n            )\n            stop_price = float(stop_price)\n            # use stop price as order price to parse it properly\n            order_or_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price\n            # type is TAKE_STOP_LIMIT (not unified)\n            if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) == \"take_stop_limit\":\n                # unsupported: no way to figure out if this order is a stop loss or a take profit\n                # (trigger above or bellow)\n                order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = (\n                    trading_enums.TradeOrderType.UNSUPPORTED.value)\n                self.logger.info(f\"Unsupported order fetched: {order_or_trade}\")\n            else:\n                if stop_price <= order_creation_price:\n                    trigger_above = False\n                    if is_selling:\n                        order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                        order_or_trade[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] = stop_price\n                    else:\n                        order_type = trading_enums.TradeOrderType.LIMIT.value\n                else:\n                    trigger_above = True\n                    if is_selling:\n                        order_type = trading_enums.TradeOrderType.LIMIT.value\n                    else:\n                        order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                        order_or_trade[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] = stop_price\n                order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above\n                order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        self._update_stop_order_or_trade_type_and_price(fixed)\n        try:\n            info = fixed[ccxt_constants.CCXT_INFO]\n            fixed[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = info[\"orderId\"]\n        except KeyError:\n            pass\n        return fixed\n\n    def fix_trades(self, raw, **kwargs):\n        fixed = super().fix_trades(raw, **kwargs)\n        for trade in fixed:\n            self._update_stop_order_or_trade_type_and_price(trade)\n        return fixed\n\n    def fix_market_status(self, raw, remove_price_limits=False, **kwargs):\n        fixed = super().fix_market_status(raw, remove_price_limits=remove_price_limits, **kwargs)\n        if not fixed:\n            return fixed\n        # bingx min and max quantity should be ignored\n        # https://bingx-api.github.io/docs/#/en-us/spot/market-api.html#Spot%20trading%20symbols\n        limits = fixed[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value]\n        limits[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT.value][\n            trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT_MIN.value\n        ] = 0\n        limits[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT.value][\n            trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_AMOUNT_MAX.value\n        ] = None\n\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/bingx/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bingx\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bingx/resources/bingx.md",
    "content": "Bingx is a RestExchange adaptation for Bingx exchange using the REST API. \n"
  },
  {
    "path": "Trading/Exchange/bingx/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bingx_websocket_feed/__init__.py",
    "content": "from .bingx_websocket import BingxCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/bingx_websocket_feed/bingx_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.bingx.bingx_exchange as bingx_exchange\n\n\nclass BingxCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: False,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        # disabled as too unstable for now (using ccxt 4.1.82)\n        # => feeds are disconnecting and not reconnecting\n        return f\"{bingx_exchange.Bingx.get_name()}-disabled\"\n\n    def get_adapter_class(self, adapter_class):\n        return bingx_exchange.BingxCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/bingx_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BingxCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitfinex/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .bitfinex_exchange import Bitfinex\n"
  },
  {
    "path": "Trading/Exchange/bitfinex/bitfinex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Private-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_commons.enums\nimport octobot_commons.constants\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\n\n\nclass Bitfinex(exchanges.RestExchange):\n\n    # bitfinex only supports 1, 25 and 100 size\n    # https://docs.bitfinex.com/reference#rest-public-book\n    SUPPORTED_ORDER_BOOK_LIMITS = [1, 25, 100]\n    DEFAULT_ORDER_BOOK_LIMIT = 25\n    DEFAULT_CANDLE_LIMIT = 500\n\n    @classmethod\n    def get_name(cls):\n        return 'bitfinex'\n\n    def get_adapter_class(self):\n        return BitfinexCCXTAdapter\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = 500, **kwargs: dict):\n        if \"since\" not in kwargs:\n            # prevent bitfinex from getting candles from 2014\n            tf_seconds = octobot_commons.enums.TimeFramesMinutes[time_frame] * \\\n                octobot_commons.constants.MINUTE_TO_SECONDS\n            kwargs[\"since\"] = (self.get_exchange_current_time() - tf_seconds * limit) \\\n                * octobot_commons.constants.MSECONDS_TO_SECONDS\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n\n    async def get_kline_price(self, symbol: str, time_frame: octobot_commons.enums.TimeFrames,\n                              **kwargs: dict) -> typing.Optional[list]:\n        return (await self.get_symbol_prices(symbol, time_frame, limit=1))[-1:]\n\n    async def get_order_book(self, symbol, limit=DEFAULT_ORDER_BOOK_LIMIT, **kwargs):\n        if limit is None or limit not in self.SUPPORTED_ORDER_BOOK_LIMITS:\n            self.logger.debug(f\"Trying to get_order_book with limit not {self.SUPPORTED_ORDER_BOOK_LIMITS} : ({limit})\")\n            limit = self.DEFAULT_ORDER_BOOK_LIMIT\n        return await super().get_recent_trades(symbol=symbol, limit=limit, **kwargs)\n\n\nclass BitfinexCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/bitfinex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bitfinex\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitfinex_websocket_feed/__init__.py",
    "content": "from .bitfinex_websocket import BitfinexCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/bitfinex_websocket_feed/bitfinex_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.bitfinex.bitfinex_exchange as bitfinex_exchange\n\n\nclass BitfinexCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,   # ohlcv is getting closed candles after new ones, this it not yet supported\n        Feeds.TICKER: True,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,  # ohlcv is getting closed candles after new ones, this it not yet supported\n    }\n\n    @classmethod\n    def get_name(cls):\n        return bitfinex_exchange.Bitfinex.get_name()\n"
  },
  {
    "path": "Trading/Exchange/bitfinex_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BitfinexCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitfinex_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bitfinex_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...bitfinex_websocket_feed import BitfinexCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    exchange_manager_instance = await exchanges_test_tools.create_test_exchange_manager(\n        config=config, exchange_name=BitfinexCryptofeedWebsocketConnector.get_name())\n\n    await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n        websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n        websocket_connector_class=BitfinexCryptofeedWebsocketConnector,\n        exchange_manager=exchange_manager_instance,\n        config=config,\n        symbols=[\"BTC/USDT\", \"ETH/USDT\"],\n        time_frames=[commons_enums.TimeFrames.ONE_MINUTE, commons_enums.TimeFrames.ONE_HOUR],\n        expected_pushed_channels={\n            channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value,\n            channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value,\n        },\n        time_before_assert=20\n    )\n    await exchanges_test_tools.stop_test_exchange_manager(exchange_manager_instance)\n"
  },
  {
    "path": "Trading/Exchange/bitget/__init__.py",
    "content": "from .bitget_exchange import Bitget"
  },
  {
    "path": "Trading/Exchange/bitget/bitget_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.enums as trading_enums\n\n\nclass Bitget(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n\n    @classmethod\n    def get_name(cls):\n        return 'bitget'\n\n    def get_adapter_class(self):\n        return BitgetCCXTAdapter\n\n    def get_additional_connector_config(self):\n        # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n        # (price should not be sent to market orders). Only used for buy market orders\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"createMarketBuyOrderRequiresPrice\": False  # disable quote conversion\n            }\n        }\n\n\nclass BitgetCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        self.adapt_amount_from_filled_or_cost(fixed)\n        return fixed\n\n    def fix_trades(self, raw, **kwargs):\n        raw = super().fix_trades(raw, **kwargs)\n        for trade in raw:\n            fee = trade[trading_enums.ExchangeConstantsOrderColumns.FEE.value]\n            if trading_enums.FeePropertyColumns.CURRENCY.value not in fee:\n                fee[trading_enums.FeePropertyColumns.CURRENCY.value] = fee.get(\"code\")\n        return raw\n"
  },
  {
    "path": "Trading/Exchange/bitget/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bitget\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitget/resources/bitget.md",
    "content": "Bitget is a RestExchange adaptation for Bitget exchange using the REST API. \n"
  },
  {
    "path": "Trading/Exchange/bitget/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bitget_websocket_feed/__init__.py",
    "content": "from .bitget_websocket import BitgetCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/bitget_websocket_feed/bitget_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.bitget.bitget_exchange as bitget_exchange\n\n\nclass BitgetCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return bitget_exchange.Bitget.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return bitget_exchange.BitgetCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/bitget_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BitgetCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitget_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bithumb/__init__.py",
    "content": "from .bithumb_exchange import Bithumb"
  },
  {
    "path": "Trading/Exchange/bithumb/bithumb_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\n\n\nclass Bithumb(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    @classmethod\n    def get_name(cls):\n        return 'bithumb'\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict):\n        # ohlcv limit is not working as expected, limit is doing [:-limit] but we want [-limit:]\n        candles = await super().get_symbol_prices(symbol=symbol, time_frame=time_frame, limit=limit, **kwargs)\n        if limit:\n            return candles[-limit:]\n        return candles\n"
  },
  {
    "path": "Trading/Exchange/bithumb/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bithumb\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bithumb/resources/bithumb.md",
    "content": "Bithumb is a basic RestExchange adaptation for Bithumb exchange. \n"
  },
  {
    "path": "Trading/Exchange/bithumb/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bitmart/__init__.py",
    "content": "from .bitmart_exchange import BitMart\n"
  },
  {
    "path": "Trading/Exchange/bitmart/bitmart_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\nimport ccxt.async_support\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\n\n\nclass BitMartConnector(exchanges.CCXTConnector):\n\n    def _client_factory(\n        self,\n        force_unauth,\n        keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None\n    ) -> tuple:\n        client, is_authenticated = super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)\n        if client:\n            client.handle_errors = self._patched_handle_errors_factory(client)\n        return client, is_authenticated\n\n    def _patched_handle_errors_factory(self, client: ccxt.async_support.Exchange):\n        self = client # set self to the client to use the client methods\n        def _patched_handle_errors(code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):\n            # temporary patch waiting for CCXT fix (issue in ccxt 4.5.28)\n            if response is None:\n                return None\n            #\n            # spot\n            #\n            #     {\"message\":\"Bad Request [to is empty]\",\"code\":50000,\"trace\":\"f9d46e1b-4edb-4d07-a06e-4895fb2fc8fc\",\"data\":{}}\n            #     {\"message\":\"Bad Request [from is empty]\",\"code\":50000,\"trace\":\"579986f7-c93a-4559-926b-06ba9fa79d76\",\"data\":{}}\n            #     {\"message\":\"Kline size over 500\",\"code\":50004,\"trace\":\"d625caa8-e8ca-4bd2-b77c-958776965819\",\"data\":{}}\n            #     {\"message\":\"Balance not enough\",\"code\":50020,\"trace\":\"7c709d6a-3292-462c-98c5-32362540aeef\",\"data\":{}}\n            #     {\"code\":40012,\"message\":\"You contract account available balance not enough.\",\"trace\":\"...\"}\n            #\n            # contract\n            #\n            #     {\"errno\":\"OK\",\"message\":\"INVALID_PARAMETER\",\"code\":49998,\"trace\":\"eb5ebb54-23cd-4de2-9064-e090b6c3b2e3\",\"data\":null}\n            #\n            message = self.safe_string_lower(response, 'message') # PATCH\n            isErrorMessage = (message is not None) and (message != 'ok') and (message != 'success')\n            errorCode = self.safe_string(response, 'code')\n            isErrorCode = (errorCode is not None) and (errorCode != '1000')\n            if isErrorCode or isErrorMessage:\n                feedback = self.id + ' ' + body\n                self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)\n                self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)\n                self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)\n                self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback)\n                raise ccxt.ExchangeError(feedback)  # unknown message\n            return None\n        return _patched_handle_errors\n\n    def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:\n        # use password as uid\n        creds.uid = creds.password\n        creds.password = None\n        return creds\n\n\nclass BitMart(exchanges.RestExchange):\n    FIX_MARKET_STATUS = True\n    DEFAULT_CONNECTOR_CLASS = BitMartConnector\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n    # broken: need v4 endpoint required, 10/10/25 ccxt still doesn't have it\n    # bitmart {\"msg\":\"This endpoint has been deprecated. Please refer to the document:\n    # https://developer-pro.bitmart.com/en/spot/#update-plan\",\"code\":30031}\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    @classmethod\n    def get_name(cls):\n        return 'bitmart'\n\n    def get_adapter_class(self):\n        return BitMartCCXTAdapter\n\n    def get_additional_connector_config(self):\n        # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n        # (price should not be sent to market orders). Only used for buy market orders\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"createMarketBuyOrderRequiresPrice\": False  # disable quote conversion\n            }\n        }\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        # not available on bitmart\n        return trading_constants.DEFAULT_ACCOUNT_ID\n\n\nclass BitMartCCXTAdapter(exchanges.CCXTAdapter):\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        self.adapt_amount_from_filled_or_cost(fixed)\n        if (\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] == trading_enums.TradeOrderType.MARKET.value\n            and fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value]\n                == trading_enums.OrderStatus.CANCELED.value\n            and fixed[trading_enums.ExchangeConstantsOrderColumns.FILLED.value]\n        ):\n            # consider as filled & closed (Bitmart is sometimes tagging filled market orders as \"canceled\": ignore it)\n            fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/bitmart/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BitMart\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitmart/resources/bitmart.md",
    "content": "BitMart is a RestExchange adaptation for BitMart exchange using the REST API. \n"
  },
  {
    "path": "Trading/Exchange/bitmart_websocket_feed/__init__.py",
    "content": "from .bitmart_websocket import BitMartCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/bitmart_websocket_feed/bitmart_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.bitmart.bitmart_exchange as bitmart_exchange\n\n\nclass BitMartCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return bitmart_exchange.BitMart.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return bitmart_exchange.BitMartCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/bitmart_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BitMartCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitmex/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Private-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .bitmex_exchange import *\n"
  },
  {
    "path": "Trading/Exchange/bitmex/bitmex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\n\n\nclass Bitmex(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    FIX_MARKET_STATUS = False    # todo fix precision price but not amount ? todo check\n\n    BUY_STR = \"Buy\"\n    SELL_STR = \"Sell\"\n\n    MARK_PRICE_IN_TICKER = True\n    FUNDING_IN_TICKER = True\n\n    @classmethod\n    def get_name(cls):\n        return 'bitmex'\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n"
  },
  {
    "path": "Trading/Exchange/bitmex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bitmex\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitmex/resources/bitmex.md",
    "content": "Bitmex is a basic RestExchange adaptation for Bitmex exchange. \n"
  },
  {
    "path": "Trading/Exchange/bitmex/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bitso/__init__.py",
    "content": "from .bitso_exchange import Bitso"
  },
  {
    "path": "Trading/Exchange/bitso/bitso_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\n\n\nclass Bitso(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    DEFAULT_MAX_LIMIT = 500\n    FIX_MARKET_STATUS = True\n\n\n    @classmethod\n    def get_name(cls):\n        return 'bitso'\n"
  },
  {
    "path": "Trading/Exchange/bitso/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bitso\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitso/resources/bitso.md",
    "content": "Bitso is a basic RestExchange adaptation for Bitso exchange. \n"
  },
  {
    "path": "Trading/Exchange/bitso/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bitstamp/__init__.py",
    "content": "from .bitstamp_exchange import Bitstamp"
  },
  {
    "path": "Trading/Exchange/bitstamp/bitstamp_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\n\n\nclass Bitstamp(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    DEFAULT_MAX_LIMIT = 500\n\n    @classmethod\n    def get_name(cls):\n        return 'bitstamp'\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict):\n        # ohlcv without limit is not supported, replaced by a default max limit\n        if limit is None:\n            limit = self.DEFAULT_MAX_LIMIT\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n"
  },
  {
    "path": "Trading/Exchange/bitstamp/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Bitstamp\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/bitstamp/resources/bitstamp.md",
    "content": "Bitstamp is a basic RestExchange adaptation for Bitstamp exchange. \n"
  },
  {
    "path": "Trading/Exchange/bitstamp/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bybit/__init__.py",
    "content": "from .bybit_exchange import Bybit\n"
  },
  {
    "path": "Trading/Exchange/bybit/bybit_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport typing\n\nimport ccxt\n\nimport octobot_commons.constants as commons_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.constants as constants\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.errors\n\n\nclass Bybit(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    # Bybit default take profits are market orders\n    # note: use BUY_MARKET and SELL_MARKET since in reality those are conditional market orders, which behave the same\n    # way as limit order but with higher fees\n    _BYBIT_BUNDLED_ORDERS = [trading_enums.TraderOrderType.STOP_LOSS, trading_enums.TraderOrderType.TAKE_PROFIT,\n                             trading_enums.TraderOrderType.BUY_MARKET, trading_enums.TraderOrderType.SELL_MARKET]\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on futures\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {\n                trading_enums.TraderOrderType.BUY_MARKET: _BYBIT_BUNDLED_ORDERS,\n                trading_enums.TraderOrderType.SELL_MARKET: _BYBIT_BUNDLED_ORDERS,\n                trading_enums.TraderOrderType.BUY_LIMIT: _BYBIT_BUNDLED_ORDERS,\n                trading_enums.TraderOrderType.SELL_LIMIT: _BYBIT_BUNDLED_ORDERS,\n            },\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n\n    MARK_PRICE_IN_TICKER = True\n    FUNDING_IN_TICKER = True\n\n    # set True when get_positions() is not returning empty positions and should use get_position() instead\n    REQUIRES_SYMBOL_FOR_EMPTY_POSITION = True\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    EXPECT_POSSIBLE_ORDER_NOT_FOUND_DURING_ORDER_CREATION = True  # set True when get_order() can return None\n    # (order not found) when orders are instantly filled on exchange and are not fully processed on the exchange side.\n\n    # Set True when get_open_order() can return outdated orders (cancelled or not yet created)\n    CAN_HAVE_DELAYED_CANCELLED_ORDERS = True\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    BUY_STR = \"Buy\"\n    SELL_STR = \"Sell\"\n\n    LONG_STR = BUY_STR\n    SHORT_STR = SELL_STR\n\n    # Order category. 0：normal order by default; 1：TP/SL order, Required for TP/SL order.\n    ORDER_CATEGORY = \"orderCategory\"\n    STOP_ORDERS_FILTER = \"stop\"\n    SPOT_STOP_ORDERS_FILTER = \"StopOrder\"\n    ORDER_FILTER = \"orderFilter\"\n\n    def __init__(\n        self, config, exchange_manager, exchange_config_by_exchange: typing.Optional[dict[str, dict]],\n        connector_class=None\n    ):\n        super().__init__(config, exchange_manager, exchange_config_by_exchange, connector_class=connector_class)\n        self.order_quantity_by_amount = {}\n        self.order_quantity_by_id = {}\n\n    def get_additional_connector_config(self):\n        connector_config = {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"recvWindow\": 60000,    # default is 5000, avoid time related issues\n            }\n        }\n        if not self.exchange_manager.is_future:\n            # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n            # (price should not be sent to market orders). Only used for buy market orders\n            connector_config[ccxt_constants.CCXT_OPTIONS][\n                \"createMarketBuyOrderRequiresPrice\"\n            ] = False  # disable quote conversion\n        return connector_config\n\n    def get_adapter_class(self):\n        return BybitCCXTAdapter\n\n    @classmethod\n    def get_name(cls) -> str:\n        return 'bybit'\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    async def initialize_impl(self):\n        await super().initialize_impl()\n        # ensure the authenticated account is not a unified trading account as it is not fully supported\n        await self._check_unified_account()\n\n    async def _check_unified_account(self):\n        if self.connector.client and not self.exchange_manager.exchange_only:\n            try:\n                self.connector.client.check_required_credentials()\n                enable_unified_margin, enable_unified_account = await self.connector.client.is_unified_enabled()\n                if enable_unified_margin or enable_unified_account:\n                    raise octobot_trading.errors.NotSupported(\n                        \"Ignoring Bybit exchange: \"\n                        \"Bybit unified trading accounts are not yet fully supported. To trade on Bybit, please use a \"\n                        \"standard account. You can easily switch between unified and standard using subaccounts. \"\n                        \"Transferring funds between subaccounts is free and instant.\"\n                    )\n            except ccxt.AuthenticationError:\n                # unauthenticated\n                pass\n\n    async def get_open_orders(self, symbol: str = None, since: int = None,\n                              limit: int = None, **kwargs: dict) -> list:\n        orders = await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)\n        if not self.exchange_manager.is_future:\n            kwargs = kwargs or {}\n            # include stop orders\n            kwargs[self.ORDER_FILTER] = self.SPOT_STOP_ORDERS_FILTER\n            orders += await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)\n        return orders\n\n    async def get_order(\n        self,\n        exchange_order_id: str,\n        symbol: typing.Optional[str] = None,\n        order_type: typing.Optional[trading_enums.TraderOrderType] = None,\n        **kwargs: dict\n    ) -> dict:\n        # regular get order is not supported\n        return await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol=symbol, **kwargs)\n\n    async def cancel_order(\n            self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict\n    ) -> trading_enums.OrderStatus:\n        kwargs = kwargs or {}\n        if trading_personal_data.is_stop_order(order_type):\n            kwargs[self.ORDER_FILTER] = self.SPOT_STOP_ORDERS_FILTER\n        return await super().cancel_order(\n            exchange_order_id, symbol, order_type, **kwargs\n        )\n\n    async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,\n                           price: decimal.Decimal = None, stop_price: decimal.Decimal = None,\n                           side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None,\n                           reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]:\n        if not self.exchange_manager.is_future:\n            # should be replacable by ENABLE_SPOT_BUY_MARKET_WITH_COST = True => check when upgrading to unified\n            if order_type is trading_enums.TraderOrderType.BUY_MARKET:\n                # on Bybit, market orders are in quote currency (YYY in XYZ/YYY)\n                used_price = price or current_price\n                if not used_price:\n                    raise octobot_trading.errors.NotSupported(f\"{self.get_name()} requires a price parameter to create \"\n                                                              f\"market orders as quantity is in quote currency\")\n                origin_quantity = quantity\n                quantity = quantity * used_price\n                self.order_quantity_by_amount[float(quantity)] = float(origin_quantity)\n        return await super().create_order(order_type, symbol, quantity,\n                                          price=price, stop_price=stop_price,\n                                          side=side, current_price=current_price,\n                                          reduce_only=reduce_only, params=params)\n\n    def _get_stop_trigger_direction(self, side):\n        if side == trading_enums.TradeOrderSide.SELL.value:\n            return \"bellow\"\n        return \"above\"\n\n    async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:\n        # todo make sure this still works\n        params = params or {}\n        params[\"triggerPrice\"] = price\n        if self.exchange_manager.is_future:\n            # BybitCCXTAdapter.BYBIT_TRIGGER_ABOVE_KEY required on future stop orders\n            params[BybitCCXTAdapter.BYBIT_TRIGGER_ABOVE_KEY] = self._get_stop_trigger_direction(side)\n        # else:\n        #     params[self.ORDER_CATEGORY] = 1\n        order = self.connector.adapter.adapt_order(\n            await self.connector.client.create_order(\n                symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params\n            ),\n            symbol=symbol, quantity=quantity\n        )\n        return order\n\n    async def _edit_order(self, exchange_order_id: str, order_type: trading_enums.TraderOrderType, symbol: str,\n                          quantity: float, price: float, stop_price: float = None, side: str = None,\n                          current_price: float = None, params: dict = None):\n        params = params or {}\n        if trading_personal_data.is_stop_order(order_type):\n            params[\"stop_order_id\"] = exchange_order_id\n        if stop_price is not None:\n            # params[\"stop_px\"] = stop_price\n            # params[\"stop_loss\"] = stop_price\n            params[\"triggerPrice\"] = str(stop_price)\n        return await super()._edit_order(exchange_order_id, order_type, symbol, quantity=quantity,\n                                         price=price, stop_price=stop_price, side=side,\n                                         current_price=current_price, params=params)\n\n    async def _verify_order(self, created_order, order_type, symbol, price, quantity, side, get_order_params=None):\n        return await super()._verify_order(created_order, order_type, symbol, price, quantity, side,\n                                           get_order_params=get_order_params)\n\n    async def set_symbol_partial_take_profit_stop_loss(self, symbol: str, inverse: bool,\n                                                       tp_sl_mode: trading_enums.TakeProfitStopLossMode):\n        # /contract/v3/private/position/switch-tpsl-mode\n        # from https://bybit-exchange.github.io/docs/derivativesV3/contract/#t-dv_switchpositionmode\n        params = {\n            \"symbol\": self.connector.client.market(symbol)['id'],\n            \"tpSlMode\": tp_sl_mode.value\n        }\n        try:\n            await self.connector.client.privatePostContractV3PrivatePositionSwitchTpslMode(params)\n        except ccxt.ExchangeError as e:\n            if \"same tp sl mode1\" in str(e):\n                # can't fetch the tp sl mode1 value\n                return\n            raise\n\n    def get_order_additional_params(self, order) -> dict:\n        params = {}\n        if self.exchange_manager.is_future:\n            contract = self.exchange_manager.exchange.get_pair_future_contract(order.symbol)\n            params[\"positionIdx\"] = self._get_position_idx(contract)\n            params[\"reduceOnly\"] = order.reduce_only\n        return params\n\n    def _get_margin_type_query_params(self, symbol, **kwargs):\n        if not self.exchange_manager.exchange.has_pair_future_contract(symbol):\n            raise KeyError(f\"{symbol} contract unavailable\")\n        else:\n            contract = self.exchange_manager.exchange.get_pair_future_contract(symbol)\n            kwargs = kwargs or {}\n            kwargs[ccxt_enums.ExchangePositionCCXTColumns.LEVERAGE.value] = float(contract.current_leverage)\n        return kwargs\n\n    async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: dict):\n        kwargs = self._get_margin_type_query_params(symbol, **kwargs)\n        await super().set_symbol_margin_type(symbol, isolated, **kwargs)\n\n    def get_bundled_order_parameters(self, order, stop_loss_price=None, take_profit_price=None) -> dict:\n        \"\"\"\n        Returns the updated params when this exchange supports orders created upon other orders fill\n        (ex: a stop loss created at the same time as a buy order)\n        :param order: the initial order\n        :param stop_loss_price: the bundled order stopLoss price\n        :param take_profit_price: the bundled order takeProfit price\n        :return: A dict with the necessary parameters to create the bundled order on exchange alongside the\n        base order in one request\n        \"\"\"\n        params = {}\n        if stop_loss_price is not None:\n            params[\"stopLoss\"] = str(stop_loss_price)\n        if take_profit_price is not None:\n            params[\"takeProfit\"] = str(take_profit_price)\n        return params\n\n    def _get_position_idx(self, contract):\n        # \"position_idx\" has to be set when trading futures\n        # from https://bybit-exchange.github.io/docs/inverse/#t-myposition\n        # Position idx, used to identify positions in different position modes:\n        # 0-One-Way Mode\n        # 1-Buy side of both side mode\n        # 2-Sell side of both side mode\n        if contract.is_one_way_position_mode():\n            return 0\n        else:\n            raise NotImplementedError(\n                f\"Hedge mode is not implemented yet. Please switch to One-Way position mode from the Bybit \"\n                f\"trading interface preferences of {contract.pair}\"\n            )\n            # TODO\n            # if Buy side of both side mode:\n            #     return 1\n            # else Buy side of both side mode:\n            #     return 2\n\n\nclass BybitCCXTAdapter(exchanges.CCXTAdapter):\n    # Position\n    BYBIT_BANKRUPTCY_PRICE = \"bustPrice\"\n    BYBIT_CLOSING_FEE = \"occClosingFee\"\n    BYBIT_MODE = \"positionIdx\"\n    BYBIT_TRADE_MODE = \"tradeMode\"\n    BYBIT_REALIZED_PNL = \"RealisedPnl\"\n    BYBIT_ONE_WAY = \"MergedSingle\"\n    BYBIT_ONE_WAY_DIGIT = \"0\"\n    BYBIT_HEDGE = \"BothSide\"\n    BYBIT_HEDGE_DIGITS = [\"1\", \"2\"]\n\n    # Funding\n    BYBIT_DEFAULT_FUNDING_TIME = 8 * commons_constants.HOURS_TO_SECONDS\n\n    # Orders\n    BYBIT_REDUCE_ONLY = \"reduceOnly\"\n    BYBIT_TRIGGER_ABOVE_KEY = \"triggerDirection\"\n    BYBIT_TRIGGER_ABOVE_VALUE = \"1\"\n\n    # Trades\n    EXEC_TYPE = \"execType\"\n    TRADE_TYPE = \"Trade\"\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        order_info = raw[trading_enums.ExchangeConstantsOrderColumns.INFO.value]\n        # parse reduce_only if present\n        fixed[trading_enums.ExchangeConstantsOrderColumns.REDUCE_ONLY.value] = \\\n            order_info.get(self.BYBIT_REDUCE_ONLY, False)\n        if tigger_above := order_info.get(trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value):\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = \\\n                tigger_above == self.BYBIT_TRIGGER_ABOVE_VALUE\n        status = fixed.get(trading_enums.ExchangeConstantsOrderColumns.STATUS.value)\n        if status == 'ORDER_NEW':\n            fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.OPEN.value\n        if status == 'ORDER_CANCELED':\n            fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CANCELED.value\n        if status == 'PARTIALLY_FILLED_CANCELED':\n            fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.FILLED.value\n        self._adapt_order_type(fixed)\n        if not self.connector.exchange_manager.is_future:\n            try:\n                if fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] \\\n                        == trading_enums.TradeOrderType.MARKET.value and \\\n                        fixed[trading_enums.ExchangeConstantsOrderColumns.SIDE.value] \\\n                        == trading_enums.TradeOrderSide.BUY.value:\n                    try:\n                        quantity = self.connector.exchange_manager.exchange.order_quantity_by_amount[\n                            kwargs.get(\"quantity\", fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.AMOUNT.value))\n                        ]\n                        self.connector.exchange_manager.exchange.order_quantity_by_id[\n                            fixed[ccxt_enums.ExchangeOrderCCXTColumns.ID.value]\n                        ] = quantity\n                    except KeyError:\n                        try:\n                            quantity = self.connector.exchange_manager.exchange.order_quantity_by_id[\n                                fixed[ccxt_enums.ExchangeOrderCCXTColumns.ID.value]]\n                        except KeyError:\n                            amount = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.AMOUNT.value)\n                            price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.AVERAGE.value,\n                                              fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.PRICE.value)\n                                              )\n                            quantity = amount / (price if price else 1)\n                    if fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] is None or \\\n                            fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] < quantity * 0.999:\n                        # when order status is PARTIALLY_FILLED_CANCELED but is actually filled\n                        fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = \\\n                            trading_enums.OrderStatus.OPEN.value\n                    # convert amount to have the same units as every other exchange\n                    fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] = quantity\n            except KeyError:\n                pass\n        return fixed\n\n    def _adapt_order_type(self, fixed):\n        if fixed.get(\"triggerPrice\", None):\n            if fixed.get(\"takeProfitPrice\", None):\n                # take profit are not tagged as such by ccxt, force it\n                # check take profit first as takeProfitPrice is also set for stop losses\n                fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = \\\n                    trading_enums.TradeOrderType.TAKE_PROFIT.value\n            elif fixed.get(\"stopPrice\", None):\n                # stop loss are not tagged as such by ccxt, force it\n                fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = \\\n                    trading_enums.TradeOrderType.STOP_LOSS.value\n            else:\n                self.logger.error(f\"Unknown [{self.connector.exchange_manager.exchange_name}] trigger order: {fixed}\")\n        return fixed\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n    \n    def parse_position(self, fixed, **kwargs):\n        try:\n            # todo handle contract value\n            raw_position_info = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.INFO.value)\n            size = decimal.Decimal(\n                str(fixed.get(ccxt_enums.ExchangePositionCCXTColumns.CONTRACTS.value, 0)))\n            # if size == constants.ZERO:\n            #     return {}  # Don't parse empty position\n\n            symbol = self.connector.get_pair_from_exchange(\n                fixed[ccxt_enums.ExchangePositionCCXTColumns.SYMBOL.value])\n            raw_mode = raw_position_info.get(self.BYBIT_MODE)\n            mode = trading_enums.PositionMode.ONE_WAY\n            if raw_mode == self.BYBIT_HEDGE or raw_mode in self.BYBIT_HEDGE_DIGITS:\n                mode = trading_enums.PositionMode.HEDGE\n            trade_mode = raw_position_info.get(self.BYBIT_TRADE_MODE)\n            margin_type = trading_enums.MarginType.ISOLATED if trade_mode == \"1\" else trading_enums.MarginType.CROSS\n            original_side = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.SIDE.value)\n\n            side = trading_enums.PositionSide.BOTH\n            # todo when handling cross positions\n            # side = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.SIDE.value, enums.PositionSide.UNKNOWN.value)\n            # position_side = enums.PositionSide.LONG \\\n            #     if side == enums.PositionSide.LONG.value else enums.PositionSide.SHORT\n\n            unrealized_pnl = self.safe_decimal(fixed,\n                                               ccxt_enums.ExchangePositionCCXTColumns.UNREALISED_PNL.value,\n                                               constants.ZERO)\n            liquidation_price = self.safe_decimal(fixed,\n                                                  ccxt_enums.ExchangePositionCCXTColumns.LIQUIDATION_PRICE.value,\n                                                  constants.ZERO)\n            entry_price = self.safe_decimal(fixed,\n                                            ccxt_enums.ExchangePositionCCXTColumns.ENTRY_PRICE.value,\n                                            constants.ZERO)\n            return {\n                trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value: symbol,\n                trading_enums.ExchangeConstantsPositionColumns.TIMESTAMP.value:\n                    self.connector.client.safe_value(fixed,\n                                                     ccxt_enums.ExchangePositionCCXTColumns.TIMESTAMP.value, 0),\n                trading_enums.ExchangeConstantsPositionColumns.SIDE.value: side,\n                trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value: margin_type,\n                trading_enums.ExchangeConstantsPositionColumns.SIZE.value:\n                    size if original_side == trading_enums.PositionSide.LONG.value else -size,\n                trading_enums.ExchangeConstantsPositionColumns.INITIAL_MARGIN.value:\n                    self.safe_decimal(\n                        fixed, ccxt_enums.ExchangePositionCCXTColumns.INITIAL_MARGIN.value,\n                        constants.ZERO\n                    ),\n                trading_enums.ExchangeConstantsPositionColumns.NOTIONAL.value:\n                    self.safe_decimal(\n                        fixed, ccxt_enums.ExchangePositionCCXTColumns.NOTIONAL.value, constants.ZERO\n                    ),\n                trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value:\n                    self.safe_decimal(\n                        fixed, ccxt_enums.ExchangePositionCCXTColumns.LEVERAGE.value, constants.ONE\n                    ),\n                trading_enums.ExchangeConstantsPositionColumns.UNREALIZED_PNL.value: unrealized_pnl,\n                trading_enums.ExchangeConstantsPositionColumns.REALISED_PNL.value:\n                    self.safe_decimal(\n                        fixed, self.BYBIT_REALIZED_PNL, constants.ZERO\n                    ),\n                trading_enums.ExchangeConstantsPositionColumns.LIQUIDATION_PRICE.value: liquidation_price,\n                trading_enums.ExchangeConstantsPositionColumns.CLOSING_FEE.value:\n                    self.safe_decimal(\n                        fixed, self.BYBIT_CLOSING_FEE, constants.ZERO\n                    ),\n                trading_enums.ExchangeConstantsPositionColumns.BANKRUPTCY_PRICE.value:\n                    self.safe_decimal(\n                        fixed, self.BYBIT_BANKRUPTCY_PRICE, constants.ZERO\n                    ),\n                trading_enums.ExchangeConstantsPositionColumns.ENTRY_PRICE.value: entry_price,\n                trading_enums.ExchangeConstantsPositionColumns.CONTRACT_TYPE.value:\n                    self.connector.exchange_manager.exchange.get_contract_type(symbol),\n                trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value: mode,\n            }\n        except KeyError as e:\n            self.logger.error(f\"Fail to parse position dict ({e})\")\n        return fixed\n\n    def parse_funding_rate(self, fixed, from_ticker=False, **kwargs):\n        \"\"\"\n        Bybit last funding time is not provided\n        To obtain the last_funding_time :\n        => timestamp(next_funding_time) - timestamp(BYBIT_DEFAULT_FUNDING_TIME)\n        \"\"\"\n        funding_dict = super().parse_funding_rate(fixed, from_ticker=from_ticker, **kwargs)\n        if from_ticker:\n            if ccxt_constants.CCXT_INFO not in funding_dict:\n                return {}\n            # no data in fixed when coming from ticker\n            funding_dict = fixed[ccxt_constants.CCXT_INFO]\n            funding_next_timestamp = self.get_uniformized_timestamp(\n                float(funding_dict.get(ccxt_enums.ExchangeFundingCCXTColumns.NEXT_FUNDING_TIME.value, 0))\n            )\n            funding_rate = decimal.Decimal(\n                str(funding_dict.get(ccxt_enums.ExchangeFundingCCXTColumns.FUNDING_RATE.value, constants.NaN))\n            )\n            funding_dict.update({\n                trading_enums.ExchangeConstantsFundingColumns.LAST_FUNDING_TIME.value:\n                    max(funding_next_timestamp - self.BYBIT_DEFAULT_FUNDING_TIME, 0),\n                trading_enums.ExchangeConstantsFundingColumns.FUNDING_RATE.value: funding_rate,\n                trading_enums.ExchangeConstantsFundingColumns.NEXT_FUNDING_TIME.value: funding_next_timestamp,\n                trading_enums.ExchangeConstantsFundingColumns.PREDICTED_FUNDING_RATE.value: funding_rate\n            })\n        else:\n            funding_next_timestamp = float(\n                funding_dict.get(trading_enums.ExchangeConstantsFundingColumns.NEXT_FUNDING_TIME.value, 0)\n            )\n            # patch LAST_FUNDING_TIME in tentacle\n            funding_dict.update({\n                trading_enums.ExchangeConstantsFundingColumns.LAST_FUNDING_TIME.value:\n                    max(funding_next_timestamp - self.BYBIT_DEFAULT_FUNDING_TIME, 0)\n            })\n        return funding_dict\n\n    def parse_mark_price(self, fixed, from_ticker=False, **kwargs) -> dict:\n        if from_ticker and ccxt_constants.CCXT_INFO in fixed:\n            try:\n                return {\n                    trading_enums.ExchangeConstantsMarkPriceColumns.MARK_PRICE.value:\n                        fixed[ccxt_constants.CCXT_INFO][trading_enums.ExchangeConstantsMarkPriceColumns.MARK_PRICE.value]\n                }\n            except KeyError:\n                pass\n        return {\n            trading_enums.ExchangeConstantsMarkPriceColumns.MARK_PRICE.value:\n                decimal.Decimal(fixed[\n                    trading_enums.ExchangeConstantsTickersColumns.CLOSE.value])\n        }\n\n    def fix_trades(self, raw, **kwargs):\n        if self.connector.exchange_manager.is_future:\n            raw = [\n                trade\n                for trade in raw\n                if trade[trading_enums.ExchangeConstantsOrderColumns.INFO.value].get(\n                    self.EXEC_TYPE, None) == self.TRADE_TYPE    # ignore non-trade elements (such as funding)\n            ]\n        return super().fix_trades(raw, **kwargs)\n"
  },
  {
    "path": "Trading/Exchange/bybit/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\n    \"Bybit\"\n  ],\n  \"tentacles-requirements\": []\n}\n"
  },
  {
    "path": "Trading/Exchange/bybit/resources/bybit.md",
    "content": "Bybit is a basic RestExchange adaptation for Bybit exchange.\n"
  },
  {
    "path": "Trading/Exchange/bybit/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/bybit_websocket_feed/__init__.py",
    "content": "from .bybit_websocket import BybitCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/bybit_websocket_feed/bybit_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.bybit.bybit_exchange as bybit_exchange\n\n\nclass BybitCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return bybit_exchange.Bybit.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return bybit_exchange.BybitCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/bybit_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BybitCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/coinbase/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .coinbase_exchange import Coinbase\n"
  },
  {
    "path": "Trading/Exchange/coinbase/coinbase_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\nimport decimal\nimport ccxt\nimport copy\n\nimport octobot_trading.errors\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_connector as ccxt_connector\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_client_util as ccxt_client_util\nimport octobot_trading.personal_data.orders.order_util as order_util\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.logging as logging\nimport octobot_commons.os_util as os_util\n\n\nALIASED_SYMBOLS = set()\n\n# hard code Coinbase base tier fees as long as there is no way to fetch it\n# https://www.coinbase.com/advanced-fees\nINTRO_1_TAKER_MAKER_FEES = (0.012, 0.006) # Intro 1: 1.2%, 0.6%: <1k monthly trading volume Coinbase taker fees tier\nINTRO_2_TAKER_MAKER_FEES = (0.0075, 0.0035) # Intro 2: 0.75%, 0.35%: >1k & <10k monthly trading volume Coinbase taker fees tier\n\n\n# simulate live fees considering the INTRO_1_TAKER_MAKER_FEES as the base tier fees to avoid \n# fees issues for intro 1 tier users\nDEFAULT_LIVE_TAKER_FEE_VALUE = INTRO_1_TAKER_MAKER_FEES[0]\nDEFAULT_LIVE_MAKER_FEE_VALUE = INTRO_1_TAKER_MAKER_FEES[1]\n# compute backtesting fees considering the INTRO_2_TAKER_MAKER_FEES as the base tier fees\nDEFAULT_BACKTESTING_TAKER_FEE_VALUE = INTRO_2_TAKER_MAKER_FEES[0]\nDEFAULT_BACKTESTING_MAKER_FEE_VALUE = INTRO_2_TAKER_MAKER_FEES[1]\n# disabled by default\nFORCE_COINBASE_BASE_FEES = os_util.parse_boolean_environment_var(\"FORCE_COINBASE_BASE_FEES\", \"false\")\n_MAX_CURSOR_ITERATIONS = 10\n\n\ndef _refresh_alias_symbols(client):\n    if client.markets:\n        ALIASED_SYMBOLS.update({\n            symbol\n            for symbol, market_status in client.markets.items()\n            if market_status[\"info\"].get(\"alias_to\")\n        })\n\n\ndef _coinbase_retrier(f):\n    async def coinbase_retrier_wrapper(*args, **kwargs):\n        last_error = None\n        for i in range(0, Coinbase.FAKE_RATE_LIMIT_ERROR_INSTANT_RETRY_COUNT):\n            try:\n                return await f(*args, **kwargs)\n            except (\n                octobot_trading.errors.FailedRequest, octobot_trading.errors.RateLimitExceeded, ccxt.BaseError\n            ) as err:\n                last_error = err\n                if Coinbase.INSTANT_RETRY_ERROR_CODE in str(err):\n                    # should retry instantly, error on coinbase side\n                    logging.get_logger(Coinbase.get_name()).debug(\n                        f\"{Coinbase.INSTANT_RETRY_ERROR_CODE} error on {f.__name__}(args={args[1:]} kwargs={kwargs}) \"\n                        f\"request, retrying now. Attempt {i+1} / {Coinbase.FAKE_RATE_LIMIT_ERROR_INSTANT_RETRY_COUNT}, \"\n                        f\"error: {err} ({last_error.__class__.__name__}).\"\n                    )\n                else:\n                    raise\n        last_error = last_error or RuntimeError(\"Unknown Coinbase error\")  # to be able to \"raise from\" in next line\n        raise octobot_trading.errors.FailedRequest(\n            f\"Failed Coinbase request after {Coinbase.FAKE_RATE_LIMIT_ERROR_INSTANT_RETRY_COUNT} \"\n            f\"retries on {f.__name__}(args={args[1:]} kwargs={kwargs}) due \"\n            f\"to {Coinbase.INSTANT_RETRY_ERROR_CODE} error code. \"\n            f\"Last error: {last_error} ({last_error.__class__.__name__})\"\n        ) from last_error\n    return coinbase_retrier_wrapper\n\n\nclass CoinbaseConnector(ccxt_connector.CCXTConnector):\n\n    def _client_factory(\n        self,\n        force_unauth,\n        keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None\n    ) -> tuple:\n        return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)\n\n    def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:\n        if creds.auth_token:\n            # when auth token is provided, force invalid keys\n            creds.api_key = \"ANY_KEY\"\n            creds.secret = \"ANY_KEY\"\n            creds.auth_token_header_prefix = \"Bearer \"\n        # CCXT pem key reader is not expecting users to under keys pasted as text from the coinbase UI\n        # convert \\\\n to \\n to make this format compatible as well\n        if creds.secret and \"\\\\n\" in creds.secret:\n            creds.secret = creds.secret.replace(\"\\\\n\", \"\\n\")\n        return creds\n\n    @_coinbase_retrier\n    async def _load_markets(\n        self, \n        client, \n        reload: bool, \n        market_filter: typing.Optional[typing.Callable[[dict], bool]] = None\n    ):\n        # override for retrier and populate ALIASED_SYMBOLS\n        await self._filtered_if_necessary_load_markets(client, reload, market_filter)\n        # only call _refresh_alias_symbols from here as markets just got reloaded,\n        # no market can be missing unlike when using cached markets\n        _refresh_alias_symbols(client)\n        if FORCE_COINBASE_BASE_FEES:\n            # always use base fee tiers inside OctoBot to avoid issues with coinbase high fees\n            self._apply_base_fee_tiers()\n\n    @classmethod\n    def register_simulator_connector_fee_methods(\n        cls, exchange_name: str, simulator_connector: exchanges.ExchangeSimulatorConnector\n    ):\n        if FORCE_COINBASE_BASE_FEES:\n            # only called in backtesting\n            # overrides exchange simulator connector get_fees to use backtesting fees\n            simulator_connector.get_fees = cls.simulator_connector_get_fees\n\n    @classmethod\n    def simulator_connector_get_fees(cls, symbol: str):\n        # same signature as ExchangeSimulatorConnector.get_fees\n        # force selecetd fee tier in backtesting\n        return {\n            trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value: DEFAULT_BACKTESTING_TAKER_FEE_VALUE,\n            trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value: DEFAULT_BACKTESTING_MAKER_FEE_VALUE,\n            trading_enums.ExchangeConstantsMarketPropertyColumns.FEE.value: trading_constants.CONFIG_DEFAULT_SIMULATOR_FEES\n        }\n\n    def _apply_base_fee_tiers(self):\n        taker_fee, maker_fee = self._get_base_tier_fees()\n        self.logger.info(\n            f\"Applying {self.exchange_manager.exchange_name} base fees tiers to markets: {taker_fee=}, {maker_fee=}\"\n        )\n        for market in self.client.markets.values():\n            market[trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value] = taker_fee\n            market[trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value] = maker_fee\n\n\n    def _get_base_tier_fees(self) -> (float, float):\n        return (\n            DEFAULT_LIVE_TAKER_FEE_VALUE, DEFAULT_LIVE_MAKER_FEE_VALUE\n        )\n        # TODO uncomment this in case there is a way to fetch tier 0 fees in Coinbase\n        # try:\n        #     # use ccxt default fee tiers\n        #     fee_tiers = self.client.describe()[\"fees\"][\"trading\"][\"tiers\"]\n        #     return (\n        #         fee_tiers[trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value][0][1],\n        #         fee_tiers[trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value][0][1],\n        #     )\n        # except KeyError as err:\n        #     self.logger.error(\n        #         f\"Error when getting base fee tier: {err}. Using default {DEFAULT_FEE_VALUE} value\"\n        #     )\n        #     return (\n        #         DEFAULT_TAKER_FEE_VALUE, DEFAULT_MAKER_FEE_VALUE\n        #     )\n\n    async def _edit_order_by_cancel_and_create(\n        self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType,\n        side: str, quantity: float, price: float, params: dict\n    ) -> dict:\n        if order_type == trading_enums.TraderOrderType.STOP_LOSS:\n            # can't use super()._edit_order_by_cancel_and_create when order is a stop loss as stop market orders\n            # are not supported\n            await self.client.cancel_order(exchange_order_id, symbol)\n            stop_price = price\n            price = float(\n                decimal.Decimal(str(price)) * self.exchange_manager.exchange.STOP_LIMIT_ORDER_INSTANT_FILL_PRICE_RATIO\n            )\n            local_param = copy.deepcopy(params)\n            return await self.create_limit_stop_loss_order(symbol, quantity, price, stop_price, side, params=local_param)\n        # not a stop loss: proceed with the usual edit flow\n        return await super()._edit_order_by_cancel_and_create(\n            exchange_order_id, symbol, order_type, side, quantity, price, params\n        )\n\n\n    @ccxt_client_util.converted_ccxt_common_errors\n    async def get_balance(self, **kwargs: dict):\n        \"\"\"\n        Local override to handle pagination of coinbase's max of 250 assets per request\n        fetch balance (free + used) by currency\n        :return: balance dict\n        \"\"\"\n        if not kwargs:\n            kwargs = {}\n        with self.error_describer():\n            results = await self._paginated_request(self.client.fetch_balance, params=kwargs)\n            merged_balances = {}\n            for result in results:\n                merged_balances.update(result)\n            return self.adapter.adapt_balance(merged_balances)\n\n    @_coinbase_retrier\n    async def _paginated_request(self, func, *args, **kwargs):\n        results = [await func(*args, **kwargs)]\n        if \"params\" not in kwargs:\n            kwargs[\"params\"] = {}\n        next_cursor = \"\"\n        i = 0\n        for i in range(_MAX_CURSOR_ITERATIONS):\n            if next_cursor := self._get_next_cursor(results[-1], func.__name__):\n                self.logger.info(f\"Large portfolio fetch in progress: request [{i}] processing ...\")\n                kwargs[\"params\"][\"cursor\"] = next_cursor\n                results.append(await func(*args, **kwargs))\n            else:\n                break\n        if next_cursor:\n            self.logger.error(\n                f\"Not all {self.exchange_manager.exchange_name} {func.__name__} was fetched after [{i + 1}] \"\n                f\"iterations. This is unexpected.\"\n            )\n        return results\n\n    def _get_next_cursor(self, response: dict, func_name: str) -> str:\n        try:\n            return response[ccxt_constants.CCXT_INFO][\"cursor\"]\n        except KeyError:\n            self.logger.error(\n                f\"Unexpected missing cursor key in {self.exchange_manager.exchange_name} {func_name} response info, \"\n                f\"available keys: {list(response[ccxt_constants.CCXT_INFO])}\"\n            )\n        return \"\"\n\n    @ccxt_client_util.converted_ccxt_common_errors\n    async def _ensure_auth(self):\n        # Override of ccxt_connector._ensure_auth to use get_open_orders instead and propagate authentication errors\n        try:\n            # load markets before calling _ensure_auth() to avoid fetching markets status while they are cached\n            await self._unauth_ensure_exchange_init()\n            # replace self.exchange_manager.exchange.get_balance by get_open_orders\n            # to mitigate coinbase balance cache side effect\n            if self.client.markets:\n                # fetch orders for any available symbol to ensure authentication is working\n                first_symbol = next(iter(self.client.markets.keys()))\n                await self.exchange_manager.exchange.get_open_orders(symbol=first_symbol)\n            else:\n                self.logger.error(\n                    f\"Unexpected: No [{self.exchange_manager.exchange_name}] markets loaded. Impossible to check authentication.\"\n                )\n        except (\n            octobot_trading.errors.AuthenticationError, \n            octobot_trading.errors.ExchangeProxyError, \n            ccxt.AuthenticationError\n        ):\n            # this error is critical on coinbase as it prevents loading markets: propagate it\n            raise\n        except Exception as err:\n            if self.force_authentication:\n                raise\n            # Is probably handled in exchange tentacles, important thing here is that authentication worked\n            self.logger.warning(\n                f\"Error when checking exchange connection: {err} ({err.__class__.__name__}). This should not be an issue.\"\n            )\n\n\nclass Coinbase(exchanges.RestExchange):\n    MAX_PAGINATION_LIMIT: int = 300\n    ALWAYS_REQUIRES_AUTHENTICATION = True\n    IS_SKIPPING_EMPTY_CANDLES_IN_OHLCV_FETCH = True\n    DEFAULT_CONNECTOR_CLASS = CoinbaseConnector\n\n    FAKE_RATE_LIMIT_ERROR_INSTANT_RETRY_COUNT = 5\n    INSTANT_RETRY_ERROR_CODE = \"429\"\n\n    FIX_MARKET_STATUS = True\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n\n    # text content of errors due to orders not found errors\n    EXCHANGE_ORDER_NOT_FOUND_ERRORS: typing.List[typing.Iterable[str]] = [\n        # coinbase {\"error\":\"NOT_FOUND\",\"error_details\":\"order with this orderID was not found\",\n        #   \"message\":\"order with this orderID was not found\"}\n        (\"not_found\", \"order\")\n    ]\n\n    # text content of errors due to api key permissions issues\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # coinbase ex: coinbase {\"error\":\"PERMISSION_DENIED\",\n        # \"error_details\":\"Missing required scopes\",\"message\":\"Missing required scopes\"}\n        # ExchangeError('coinbase {\"error\":\"unknown\",\"error_details\":\"Missing required scopes\",\n        # \"message\":\"Missing required scopes\"}')\n        (\"missing required scopes\", ),\n        (\"permission is required\", ),\n    ]\n    # text content of errors due to traded assets for account\n    EXCHANGE_ACCOUNT_TRADED_SYMBOL_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # ex when trading WBTC/USDC with and account that can't trade it:\n        # ccxt.base.errors.BadRequest: target is not enabled for trading\n        (\"target is not enabled for trading\", ),\n        # ccxt.base.errors.PermissionDenied: coinbase {\"error\":\"PERMISSION_DENIED\",\"error_details\":\n        # \"User is not allowed to convert crypto\",\"message\":\"User is not allowed to convert crypto\"}\n        (\"user is not allowed to convert crypto\", ),\n    ]\n    # text content of errors due to exchange internal synch (like when portfolio is not yet up to date after a trade)\n    EXCHANGE_INTERNAL_SYNC_ERRORS: typing.List[typing.Iterable[str]] = [\n        # BadRequest coinbase {\"error\":\"INVALID_ARGUMENT\",\"error_details\":\"account is not available\",\"message\":\"account is not available\"}\n        (\"account is not available\", )\n    ]\n    # text content of errors due to missing fnuds when creating an order (when not identified as such by ccxt)\n    EXCHANGE_MISSING_FUNDS_ERRORS: typing.List[typing.Iterable[str]] = [\n        (\"insufficient balance in source account\", )\n    ]\n    # text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)\n    EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [\n        ('cancelorders() has failed, check your arguments and parameters', )\n    ]\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            # not supported or need custom mechanics with batch orders\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on spot (as spot limit)\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n    # stop limit price is 2% bellow trigger price to ensure instant fill\n    STOP_LIMIT_ORDER_INSTANT_FILL_PRICE_RATIO = decimal.Decimal(\"0.98\")\n\n    @classmethod\n    def get_name(cls):\n        return 'coinbase'\n\n    def get_adapter_class(self):\n        return CoinbaseCCXTAdapter\n\n    @staticmethod\n    def get_default_reference_market(exchange_name: str) -> str:\n        return \"USDC\"\n\n    def get_alias_symbols(self) -> set[str]:\n        \"\"\"\n        :return: a set of symbol of this exchange that are aliases to other symbols\n        \"\"\"\n        return ALIASED_SYMBOLS\n\n    def supports_native_edit_order(self, order_type: trading_enums.TraderOrderType) -> bool:\n        # return False when default edit_order can't be used and order should always be canceled and recreated instead\n        # only working with regular limit orders\n        return order_type not in (\n            trading_enums.TraderOrderType.STOP_LOSS, trading_enums.TraderOrderType.STOP_LOSS_LIMIT\n        )\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        try:\n            with self.connector.error_describer():\n                accounts = await self.connector.client.fetch_accounts()\n                # use portfolio id when possible to enable \"coinbase subaccounts\" which are called \"portfolios\"\n                # note: oldest portfolio portfolio id == user id (from previous v2PrivateGetUser) when\n                # using master account\n                portfolio_ids = set(account[ccxt_constants.CCXT_INFO]['retail_portfolio_id'] for account in accounts)\n                if len(portfolio_ids) != 1:\n                    is_up_to_date_key = self._is_up_to_date_api_key()\n                    if is_up_to_date_key:\n                        self.logger.error(\n                            f\"Unexpected: failed to identify Coinbase portfolio id on up to date API keys: \"\n                            f\"{portfolio_ids=}\"\n                        )\n                    sorted_portfolios = sorted(\n                        [\n                            account[ccxt_constants.CCXT_INFO]\n                            for account in accounts\n                        ],\n                        key=lambda account: account[\"created_at\"],\n                    )\n                    portfolio_id = sorted_portfolios[0]['retail_portfolio_id']\n                    self.logger.info(\n                        f\"{len(portfolio_ids)} portfolio found on Coinbase account. \"\n                        f\"This can happen with non up-to-date API keys ({is_up_to_date_key=}). \"\n                        f\"Using the oldest portfolio id to bind to main account: {portfolio_id=}.\"\n                    )\n                else:\n                    portfolio_id = next(iter(portfolio_ids))\n                return portfolio_id\n        except ccxt.AuthenticationError:\n            raise\n        except (ccxt.BaseError, octobot_trading.errors.OctoBotExchangeError) as err:\n            self.logger.exception(\n                err, True,\n                f\"Error when fetching {self.get_name()} account id: {err} ({err.__class__.__name__}). \"\n                f\"This is not normal, endpoint might be deprecated, see \"\n                f\"https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-users. \"\n                f\"Using generated account id instead\"\n            )\n            return trading_constants.DEFAULT_ACCOUNT_ID\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        # unknown (05/06/2025)\n        return super().get_max_orders_count(symbol, order_type)\n\n    def _is_up_to_date_api_key(self) -> bool:\n        return (\n            self.connector.client.apiKey.find('organizations/') >= 0 or\n            self.connector.client.apiKey.startswith('-----BEGIN')\n        )\n\n\n    @_coinbase_retrier\n    async def get_symbol_prices(self, symbol: str, time_frame: commons_enums.TimeFrames, limit: int = None,\n                                **kwargs: dict) -> typing.Optional[list]:\n        return await super().get_symbol_prices(\n            symbol, time_frame, **self._get_ohlcv_params(time_frame, limit, **kwargs)\n        )\n\n    @_coinbase_retrier\n    async def get_recent_trades(self, symbol, limit=50, **kwargs):\n        # override for retrier\n        return await super().get_recent_trades(symbol, limit=limit, **kwargs)\n\n    @_coinbase_retrier\n    async def get_price_ticker(self, symbol: str, **kwargs: dict) -> typing.Optional[dict]:\n        # override for retrier\n        return await super().get_price_ticker(symbol, **kwargs)\n\n    @_coinbase_retrier\n    async def get_all_currencies_price_ticker(self, **kwargs: dict) -> typing.Optional[dict[str, dict]]:\n        # override for retrier\n        return await super().get_all_currencies_price_ticker(**kwargs)\n\n    @_coinbase_retrier\n    async def cancel_order(\n        self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict\n    ) -> trading_enums.OrderStatus:\n        # override for retrier\n        return await super().cancel_order(exchange_order_id, symbol, order_type, **kwargs)\n\n    async def get_balance(self, **kwargs: dict):\n        # warning: sometimes has unexpected delays after creating / filling orders\n        if \"v3\" not in kwargs:\n            # use v3 to get free and total amounts (default is only returning free amounts)\n            kwargs[\"v3\"] = True\n        return await super().get_balance(**kwargs)\n\n    @_coinbase_retrier\n    async def _create_order_with_retry(self, order_type, symbol, quantity: decimal.Decimal,\n                                       price: decimal.Decimal, stop_price: decimal.Decimal,\n                                       side: trading_enums.TradeOrderSide,\n                                       current_price: decimal.Decimal,\n                                       reduce_only: bool, params) -> dict:\n        # override for retrier\n        return await super()._create_order_with_retry(\n            order_type=order_type, symbol=symbol, quantity=quantity, price=price,\n            stop_price=stop_price, side=side, current_price=current_price,\n            reduce_only=reduce_only, params=params\n        )\n\n    @_coinbase_retrier\n    async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        # override for retrier\n        return await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)\n\n    @_coinbase_retrier\n    async def get_order(\n        self,\n        exchange_order_id: str,\n        symbol: typing.Optional[str] = None,\n        order_type: typing.Optional[trading_enums.TraderOrderType] = None,\n        **kwargs: dict\n    ) -> dict:\n        # override for retrier\n        return await super().get_order(exchange_order_id, symbol=symbol, order_type=order_type, **kwargs)\n\n    async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:\n        # warning coinbase only supports stop limit orders, stop markets are not available\n        stop_price = price\n        price = float(decimal.Decimal(str(price)) * self.STOP_LIMIT_ORDER_INSTANT_FILL_PRICE_RATIO)\n        # use limit stop loss with a \"normally instantly\" filled price\n        return await self._create_limit_stop_loss_order(symbol, quantity, price, stop_price, side, params=params)\n\n    def _get_ohlcv_params(self, time_frame, input_limit, **kwargs):\n        limit = input_limit\n        if not input_limit or input_limit > self.MAX_PAGINATION_LIMIT:\n            limit = min(self.MAX_PAGINATION_LIMIT, input_limit) if input_limit else self.MAX_PAGINATION_LIMIT\n        if \"since\" not in kwargs:\n            time_frame_sec = commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MSECONDS_TO_MINUTE\n            to_time = self.connector.client.milliseconds()\n            kwargs[\"since\"] = to_time - (time_frame_sec * limit)\n            kwargs[\"limit\"] = limit\n        return kwargs\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        signature_identifier = \"CB-ACCESS-SIGN\"\n        oauth_identifier = \"Authorization\"\n        return bool(\n            headers\n            and (\n                signature_identifier in headers\n                or oauth_identifier in headers\n            )\n        )\n\n    def is_market_open_for_order_type(self, symbol: str, order_type: trading_enums.TraderOrderType) -> bool:\n        \"\"\"\n        Override if necessary\n        \"\"\"\n        market_status_info = self.get_market_status(symbol, with_fixer=False).get(ccxt_constants.CCXT_INFO, {})\n        trade_order_type = order_util.get_trade_order_type(order_type)\n        try:\n            if trade_order_type is trading_enums.TradeOrderType.MARKET:\n                return not market_status_info[\"limit_only\"]\n            if trade_order_type is trading_enums.TradeOrderType.LIMIT:\n                return not market_status_info[\"cancel_only\"]\n        except KeyError as err:\n            self.logger.exception(\n                err,\n                True,\n                f\"Can't check {self.get_name()} market opens status for order type: missing {err} \"\n                f\"in market status info. {self.get_name()} API probably changed. Considering market as open. \"\n                f\"market_status_info: {market_status_info}\"\n            )\n        return True\n\n\nclass CoinbaseCCXTAdapter(exchanges.CCXTAdapter):\n\n    def _register_exchange_fees(self, order_or_trade):\n        super()._register_exchange_fees(order_or_trade)\n        try:\n            fees = order_or_trade[trading_enums.ExchangeConstantsOrderColumns.FEE.value]\n            if not fees[trading_enums.FeePropertyColumns.CURRENCY.value]:\n                # fees currency are not provided, they are always in quote on Coinbase\n                fees[trading_enums.FeePropertyColumns.CURRENCY.value] = commons_symbols.parse_symbol(\n                    order_or_trade[trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value]\n                ).quote\n        except (KeyError, TypeError):\n            pass\n\n    def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):\n        if stop_price := order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value):\n            # from https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders\n            limit_price = order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.PRICE.value)\n            # use stop price as order price to parse it properly\n            order_or_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price\n            # type is TAKE_STOP_LIMIT (not unified)\n            if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) not in (\n                trading_enums.TradeOrderType.STOP_LOSS.value, trading_enums.TradeOrderType.TAKE_PROFIT.value\n            ):\n                # Force stop loss. Add order direction parsing logic to handle take profits if necessary\n                order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                trigger_above = False\n                try:\n                    order_config = order_or_trade.get(ccxt_constants.CCXT_INFO, {}).get(\"order_configuration\", {})\n                    stop_config = order_config.get(\"stop_limit_stop_limit_gtc\") or order_config.get(\"stop_limit_stop_limit_gtd\")\n                    stop_direction = stop_config.get(\"stop_direction\", \"\")\n                    if \"down\" in stop_direction.lower():\n                        trigger_above = False\n                    elif \"up\" in stop_direction.lower():\n                        trigger_above = True\n                    else:\n                        self.logger.error(f\"Unknown order direction: {stop_direction} ({order_or_trade})\")\n                    side = order_or_trade[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]\n                    if side == trading_enums.TradeOrderSide.SELL.value:\n                        if trigger_above:\n                            # take profits are not yet handled as such: consider them as limit orders\n                            order_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling\n                        else:\n                            order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                    elif side == trading_enums.TradeOrderSide.BUY.value:\n                        if trigger_above:\n                            order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                        else:\n                            # take profits are not yet handled as such: consider them as limit orders\n                            order_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling\n                except (KeyError, TypeError) as err:\n                    self.logger.error(f\"missing expected coinbase order config: {err}, {order_or_trade}\")\n                order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type\n                order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above\n\n    def fix_order(self, raw, **kwargs):\n        \"\"\"\n        Handle 'order_type': 'UNKNOWN_ORDER_TYPE in coinbase order response (translated into None in ccxt order type)\n        ex:\n        {'info': {'order_id': 'd7471b4e-960e-4c92-bdbf-755cb92e176b', 'product_id': 'AAVE-USD',\n        'user_id': '9868efd7-90e1-557c-ac0e-f6b943d471ad', 'order_configuration': {'limit_limit_gtc':\n        {'base_size': '6.798', 'limit_price': '110.92', 'post_only': False}}, 'side': 'BUY',\n        'client_order_id': '465ead64-6272-4e92-97e2-59653de3ca24', 'status': 'OPEN', 'time_in_force':\n        'GOOD_UNTIL_CANCELLED', 'created_time': '2024-03-02T03:04:11.070126Z', 'completion_percentage':\n        '0', 'filled_size': '0', 'average_filled_price': '0', 'fee': '', 'number_of_fills': '0', 'filled_value': '0',\n        'pending_cancel': False, 'size_in_quote': False, 'total_fees': '0', 'size_inclusive_of_fees': False,\n        'total_value_after_fees': '757.05029664', 'trigger_status': 'INVALID_ORDER_TYPE', 'order_type':\n        'UNKNOWN_ORDER_TYPE', 'reject_reason': 'REJECT_REASON_UNSPECIFIED', 'settled': False, 'product_type':\n        'SPOT', 'reject_message': '', 'cancel_message': '', 'order_placement_source': 'RETAIL_ADVANCED',\n        'outstanding_hold_amount': '757.05029664', 'is_liquidation': False, 'last_fill_time': None,\n        'edit_history': [], 'leverage': '', 'margin_type': 'UNKNOWN_MARGIN_TYPE'}, 'clientOrderId':\n        '465ead64-6272-4e92-97e2-59653de3ca24', 'timestamp': 1709348651.07, 'datetime': '2024-03-02T03:04:11.070126Z',\n        'lastTradeTimestamp': None, 'symbol': 'AAVE/USD', 'type': None, 'timeInForce': 'GTC', 'postOnly': False,\n        'side': 'buy', 'price': 110.92, 'stopPrice': None, 'triggerPrice': None, 'amount': 6.798, 'filled': 0.0,\n        'remaining': 6.798, 'cost': 0.0, 'average': None, 'status': 'open', 'fee': {'cost': '0', 'currency': 'USD',\n        'exchange_original_cost': '0', 'is_from_exchange': True}, 'trades': [],\n        'fees': [{'cost': 0.0, 'currency': 'USD'}], 'lastUpdateTimestamp': None, 'reduceOnly': None,\n        'takeProfitPrice': None, 'stopLossPrice': None, 'exchange_id': 'd7471b4e-960e-4c92-bdbf-755cb92e176b'}\n        \"\"\"\n        fixed = super().fix_order(raw, **kwargs)\n        self._update_stop_order_or_trade_type_and_price(fixed)\n        if fixed[ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value] is None:\n            if fixed[ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value] is not None:\n                # stop price set: stop order\n                order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n            elif fixed[ccxt_enums.ExchangeOrderCCXTColumns.PRICE.value] is None:\n                # price not set: market order\n                order_type = trading_enums.TradeOrderType.MARKET.value\n            else:\n                # price is set and stop price is not: limit order\n                order_type = trading_enums.TradeOrderType.LIMIT.value\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type\n        if fixed[ccxt_enums.ExchangeOrderCCXTColumns.STATUS.value] == \"PENDING\":\n            fixed[ccxt_enums.ExchangeOrderCCXTColumns.STATUS.value] = trading_enums.OrderStatus.PENDING_CREATION.value\n        if fixed[ccxt_enums.ExchangeOrderCCXTColumns.STATUS.value] == \"CANCEL_QUEUED\":\n            fixed[ccxt_enums.ExchangeOrderCCXTColumns.STATUS.value] = trading_enums.OrderStatus.PENDING_CANCEL.value\n        # sometimes amount is not set\n        if not fixed[ccxt_enums.ExchangeOrderCCXTColumns.AMOUNT.value] \\\n                and fixed[ccxt_enums.ExchangeOrderCCXTColumns.FILLED.value]:\n            fixed[ccxt_enums.ExchangeOrderCCXTColumns.AMOUNT.value] = \\\n                fixed[ccxt_enums.ExchangeOrderCCXTColumns.FILLED.value]\n        return fixed\n\n    def fix_trades(self, raw, **kwargs):\n        raw = super().fix_trades(raw, **kwargs)\n        for trade in raw:\n            self._update_stop_order_or_trade_type_and_price(trade)\n            trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value\n            try:\n                if trade[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] is None and \\\n                        trade[trading_enums.ExchangeConstantsOrderColumns.COST.value] and \\\n                        trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:\n                    # convert amount to have the same units as every other exchange\n                    trade[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] = (\n                            trade[trading_enums.ExchangeConstantsOrderColumns.COST.value] /\n                            trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]\n                    )\n            except KeyError:\n                pass\n        return raw\n"
  },
  {
    "path": "Trading/Exchange/coinbase/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Coinbase\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/coinbase_pro/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .coinbase_pro_exchange import CoinbasePro\n"
  },
  {
    "path": "Trading/Exchange/coinbase_pro/coinbase_pro_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\n\n\nclass CoinbasePro(exchanges.RestExchange):\n    MAX_PAGINATION_LIMIT: int = 100  # value from https://docs.pro.coinbase.com/#pagination\n\n    FIX_MARKET_STATUS = True\n\n    @classmethod\n    def get_name(cls):\n        return 'coinbasepro'\n\n    def get_adapter_class(self):\n        return CoinbaseProCCXTAdapter\n\n    async def get_my_recent_trades(self, symbol=None, since=None, limit=None, **kwargs):\n        return self._uniformize_trades(await super().get_my_recent_trades(symbol=symbol,\n                                                                          since=since,\n                                                                          limit=self._fix_limit(limit),\n                                                                          **kwargs))\n\n    async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await super().get_open_orders(symbol=symbol,\n                                             since=since,\n                                             limit=self._fix_limit(limit),\n                                             **kwargs)\n\n    async def get_closed_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await super().get_closed_orders(symbol=symbol,\n                                               since=since,\n                                               limit=self._fix_limit(limit),\n                                               **kwargs)\n\n    def _fix_limit(self, limit: int) -> int:\n        return min(self.MAX_PAGINATION_LIMIT, limit) if limit else limit\n\n    def _uniformize_trades(self, trades):\n        if not trades:\n            return []\n        for trade in trades:\n            trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value\n            trade[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = trade[\n                trading_enums.ExchangeConstantsOrderColumns.ORDER.value\n            ]\n            trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = trading_enums.TradeOrderType.MARKET.value \\\n                if trade[\"takerOrMaker\"] == trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value \\\n                else trading_enums.TradeOrderType.LIMIT.value\n        return trades\n\n\nclass CoinbaseProCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_trades(self, raw, **kwargs):\n        raw = super().fix_trades(raw, **kwargs)\n        for trade in raw:\n            trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value\n        return raw\n"
  },
  {
    "path": "Trading/Exchange/coinbase_pro/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CoinbasePro\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/coinbase_pro_websocket_feed/__init__.py",
    "content": "from .coinbase_pro_websocket import CoinbaseProCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/coinbase_pro_websocket_feed/coinbase_pro_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.coinbase_pro.coinbase_pro_exchange as coinbase_pro_exchange\n\n\nclass CoinbaseProCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return coinbase_pro_exchange.CoinbasePro.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return coinbase_pro_exchange.CoinbaseProCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/coinbase_pro_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CoinbaseProCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/coinbase_pro_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/coinbase_pro_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...coinbase_pro_websocket_feed import CoinbaseProCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    async with websocket_test_tools.ws_exchange_manager(config, CoinbaseProCryptofeedWebsocketConnector.get_name()) \\\n            as exchange_manager_instance:\n        await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n            websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n            websocket_connector_class=CoinbaseProCryptofeedWebsocketConnector,\n            exchange_manager=exchange_manager_instance,\n            config=config,\n            symbols=[\"BTC/USD\", \"ETH/USD\"],\n            time_frames=[commons_enums.TimeFrames.ONE_HOUR],\n            expected_pushed_channels={\n                channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value\n            },\n            time_before_assert=20\n        )\n"
  },
  {
    "path": "Trading/Exchange/coinbase_websocket_feed/__init__.py",
    "content": "from .coinbase_websocket import CoinbaseCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/coinbase_websocket_feed/coinbase_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.coinbase.coinbase_exchange as coinbase_exchange\n\n\nclass CoinbaseCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return coinbase_exchange.Coinbase.get_name()\n\n    def _get_keys_adapter(self):\n        return self.exchange_manager.exchange.connector._keys_adapter\n\n    def get_adapter_class(self, adapter_class):\n        return coinbase_exchange.CoinbaseCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/coinbase_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CoinbaseCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/coinex/__init__.py",
    "content": "from .coinex_exchange import Coinex"
  },
  {
    "path": "Trading/Exchange/coinex/coinex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as constants\n\n\nclass Coinex(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    MAX_PAGINATION_LIMIT: int = 100\n\n    # text content of errors due to orders not found errors\n    EXCHANGE_ORDER_NOT_FOUND_ERRORS: typing.List[typing.Iterable[str]] = [\n        # ExchangeError('coinex Order not found')\n        (\"order not found\", )\n    ]\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n\n    @classmethod\n    def get_name(cls):\n        return 'coinex'\n\n    def get_adapter_class(self):\n        return CoinexCCXTAdapter\n\n    def get_additional_connector_config(self):\n        # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n        # (price should not be sent to market orders). Only used for buy market orders\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"createMarketBuyOrderRequiresPrice\": False  # disable quote conversion\n            }\n        }\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        # current impossible to get account UID (22/02/25)\n        return constants.DEFAULT_ACCOUNT_ID\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        v1_signature_identifiers = \"Authorization\"\n        v2_signature_identifiers = \"X-COINEX-SIGN\"\n        return bool(\n            headers\n            and (\n                v1_signature_identifiers in headers\n                or v2_signature_identifiers in headers\n            )\n        )\n\n    async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await super().get_open_orders(symbol=symbol,\n                                             since=since,\n                                             limit=self._fix_limit(limit),\n                                             **kwargs)\n\n    async def get_closed_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await super().get_closed_orders(symbol=symbol,\n                                               since=since,\n                                               limit=self._fix_limit(limit),\n                                               **kwargs)\n\n    def _fix_limit(self, limit: int) -> int:\n        return min(self.MAX_PAGINATION_LIMIT, limit) if limit else limit\n\n\nclass CoinexCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        self.adapt_amount_from_filled_or_cost(fixed)\n        try:\n            if fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] is None:\n                fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = \\\n                    trading_enums.OrderStatus.CLOSED.value\n            # from https://docs.coinex.com/api/v2/enum#order_status\n            elif fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] == \"part_filled\":\n                # order partially executed (still pending)\n                fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = \\\n                    trading_enums.OrderStatus.OPEN.value\n            elif fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] == \"part_canceled\":\n                # order partially executed and then canceled\n                fixed[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = \\\n                    trading_enums.OrderStatus.CANCELED.value\n        except KeyError:\n            pass\n        return fixed\n\n    def fix_trades(self, raw, **kwargs):\n        raw = super().fix_trades(raw, **kwargs)\n        for trade in raw:\n            info = trade[ccxt_constants.CCXT_INFO]\n            # fees are not parsed by ccxt\n            fee = trade[trading_enums.ExchangeConstantsOrderColumns.FEE.value] or {}\n            if not fee.get(trading_enums.FeePropertyColumns.CURRENCY.value):\n                fee[trading_enums.FeePropertyColumns.CURRENCY.value] = info.get(\"fee_ccy\")\n            if not fee.get(trading_enums.FeePropertyColumns.COST.value):\n                fee[trading_enums.FeePropertyColumns.COST.value] = info.get(\"fee\")\n            trade[trading_enums.ExchangeConstantsOrderColumns.FEE.value] = fee\n            self._register_exchange_fees(trade)\n        return raw\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/coinex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Coinex\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/coinex/resources/coinex.md",
    "content": "coinex is a basic RestExchange adaptation for coinex exchange. \n"
  },
  {
    "path": "Trading/Exchange/coinex_websocket_feed/__init__.py",
    "content": "from .coinex_websocket import CoinexCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/coinex_websocket_feed/coinex_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.coinex.coinex_exchange as coinex_exchange\n\n\nclass CoinexCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,  # only for swap markets (futures)\n        Feeds.TICKER: True,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,  # only for swap markets (futures)\n    }\n\n    @classmethod\n    def get_name(cls):\n        return coinex_exchange.Coinex.get_name()\n"
  },
  {
    "path": "Trading/Exchange/coinex_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CoinexCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/configurable_default_ccxt_rest/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Private-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .configurable_default_rest_ccxt_exchange import *\n"
  },
  {
    "path": "Trading/Exchange/configurable_default_ccxt_rest/configurable_default_rest_ccxt_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\n\n\nclass ConfigurableDefaultCCXTRestExchange(exchanges.DefaultRestExchange):\n\n    @classmethod\n    def load_user_inputs_from_class(cls, tentacles_setup_config, tentacle_config):\n        # bypass parent to use the real load_user_inputs and enable user inputs configuration\n        return exchanges.RestExchange.load_user_inputs_from_class(tentacles_setup_config, tentacle_config)\n"
  },
  {
    "path": "Trading/Exchange/configurable_default_ccxt_rest/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ConfigurableDefaultCCXTRestExchange\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/configurable_default_ccxt_rest/resources/configurable_default_rest_ccxt_exchange.md",
    "content": "ConfigurableDefaultCCXTRestExchange is a basic RestExchange that supports user input configuration. \n"
  },
  {
    "path": "Trading/Exchange/cryptocom/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Private-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .cryptocom_exchange import *\n"
  },
  {
    "path": "Trading/Exchange/cryptocom/cryptocom_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\n\n\nclass CryptoCom(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n    EXPECT_POSSIBLE_ORDER_NOT_FOUND_DURING_ORDER_CREATION = True  # set True when get_order() can return None\n    # (order not found) when orders are instantly filled on exchange and are not fully processed on the exchange side.\n\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n\n    @classmethod\n    def get_name(cls):\n        return 'cryptocom'\n"
  },
  {
    "path": "Trading/Exchange/cryptocom/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CryptoCom\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/cryptocom_websocket_feed/__init__.py",
    "content": "from .cryptocom_websocket import CryptoComCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/cryptocom_websocket_feed/cryptocom_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.cryptocom.cryptocom_exchange as cryptocom_exchange\n\n\nclass CryptoComCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return cryptocom_exchange.CryptoCom.get_name()\n"
  },
  {
    "path": "Trading/Exchange/cryptocom_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"CryptoComCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/gateio/__init__.py",
    "content": "from .gateio_exchange import GateIO"
  },
  {
    "path": "Trading/Exchange/gateio/gateio_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\n\nimport typing\n\n\nclass GateIO(exchanges.RestExchange):\n    ORDERS_LIMIT = 100\n\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n\n    # text content of errors due to unhandled IP white list issues\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # gateio {\"message\":\"Request IP not in whitelist: 11.11.11.11\",\"label\":\"FORBIDDEN\"}\n        (\"ip not in whitelist\",),\n    ]\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    @classmethod\n    def get_name(cls):\n        return 'gateio'\n\n    def get_adapter_class(self):\n        return GateioCCXTAdapter\n\n    async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await super().get_open_orders(symbol=symbol,\n                                             since=since,\n                                             limit=min(self.ORDERS_LIMIT, limit) \n                                                   if limit is not None else None,\n                                             **kwargs)\n\n\nclass GateioCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/gateio/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GateIO\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/gateio/resources/gateio.md",
    "content": "GateIO is a basic RestExchange adaptation for GateIO exchange. \n"
  },
  {
    "path": "Trading/Exchange/gateio_websocket_feed/__init__.py",
    "content": "from .gateio_websocket import GateIOCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/gateio_websocket_feed/gateio_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.gateio.gateio_exchange as gateio_exchange\n\n\nclass GateIOCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return gateio_exchange.GateIO.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return gateio_exchange.GateioCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/gateio_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GateIOCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/gateio_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/gateio_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...gateio_websocket_feed import GateIOCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    exchange_manager_instance = await exchanges_test_tools.create_test_exchange_manager(\n        config=config, exchange_name=GateIOCryptofeedWebsocketConnector.get_name())\n\n    await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n        websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n        websocket_connector_class=GateIOCryptofeedWebsocketConnector,\n        exchange_manager=exchange_manager_instance,\n        config=config,\n        symbols=[\"BTC/USDT\", \"ETH/USDT\"],\n        time_frames=[commons_enums.TimeFrames.ONE_MINUTE, commons_enums.TimeFrames.ONE_HOUR],\n        expected_pushed_channels={\n            channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value,\n        },\n        time_before_assert=20\n    )\n    await exchanges_test_tools.stop_test_exchange_manager(exchange_manager_instance)\n"
  },
  {
    "path": "Trading/Exchange/hitbtc/__init__.py",
    "content": "from .hitbtc_exchange import Hitbtc"
  },
  {
    "path": "Trading/Exchange/hitbtc/hitbtc_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.exchanges as exchanges\n\n\nclass Hitbtc(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    @classmethod\n    def get_name(cls):\n        return 'hitbtc'\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict):\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, sort='DESC', **kwargs)\n"
  },
  {
    "path": "Trading/Exchange/hitbtc/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Hitbtc\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/hitbtc/resources/hitbtc.md",
    "content": "Hitbtc is a basic RestExchange adaptation for Hitbtc exchange. \n"
  },
  {
    "path": "Trading/Exchange/hitbtc/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/hollaex/__init__.py",
    "content": "from .hollaex_exchange import hollaex"
  },
  {
    "path": "Trading/Exchange/hollaex/config/hollaex.json",
    "content": "{\n    \"rest\": \"https://api.hollaex.com\"\n}"
  },
  {
    "path": "Trading/Exchange/hollaex/hollaex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ccxt\nimport typing\nimport decimal\nimport enum\nimport cachetools\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as symbols_utils\nimport octobot_commons.logging as logging\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.errors as errors\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_clients_cache as ccxt_clients_cache\n\n\n_EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME: dict[str, dict] = {}\n# refresh exchange fee tiers every day but don't delete outdated info, only replace it with updated ones\n_REFRESHED_EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME : cachetools.TTLCache[str, bool] = cachetools.TTLCache(\n    maxsize=50, ttl=commons_constants.DAYS_TO_SECONDS\n)\nDEFAULT_FEE_SIDE = trading_enums.ExchangeFeeSides.GET.value     # the fee is always in the currency you get\n\n\nclass FeeTiers(enum.Enum):\n    BASIC = \"1\"\n    VIP = \"2\"\n\n\nclass hollaexConnector(exchanges.CCXTConnector):\n\n    async def load_symbol_markets(\n        self,\n        reload=False,\n        market_filter: typing.Union[None, typing.Callable[[dict], bool]] = None\n    ):\n        await super().load_symbol_markets(reload=reload, market_filter=market_filter)\n        await self.disable_quick_trade_only_pairs()\n        # also refresh fee tiers when necessary\n        if self.exchange_manager.exchange_name not in _REFRESHED_EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME:\n            authenticated_cache = self.exchange_manager.exchange.requires_authentication_for_this_configuration_only()\n            # always update fees cache using all markets to avoid market filter side effects from the current client\n            all_markets = ccxt_clients_cache.get_exchange_parsed_markets(\n                ccxt_clients_cache.get_client_key(self.client, authenticated_cache)\n            )\n            await self._refresh_exchange_fee_tiers(all_markets)\n\n    async def disable_quick_trade_only_pairs(self):\n        # on hollaex exchanges, a market can be \"quick trade only\" or \"spot order book trade\" as well.\n        # a quick trade only market can't be traded like a spot market, disable it.\n        exchange_constants = await self.client.publicGetConstants()\n        quick_trade_only_pairs = self._parse_quick_trades_only_pairs(exchange_constants)\n        if disabled_pairs := [\n            pair \n            for pair in quick_trade_only_pairs\n            if pair in self.client.markets\n        ]:\n            self.logger.info(\n                f\"Disabling [{self.exchange_manager.exchange_name}] {len(disabled_pairs)} quick trade only pairs: {disabled_pairs}\"\n            )\n            for disabled_pair in disabled_pairs:\n                self._disable_pair(disabled_pair)\n\n    def _disable_pair(self, symbol: str):\n        if symbol in self.client.markets:\n            self.client.markets[symbol][trading_enums.ExchangeConstantsMarketStatusColumns.ACTIVE.value] = False\n\n    def _parse_quick_trades_only_pairs(self, exchange_constants: dict) -> list[str]:\n        if 'quicktrade' not in exchange_constants:\n            self.logger.error(\n                f\"Unexpected [{self.exchange_manager.exchange_name}] no 'quicktrade' key found in exchange constants\"\n            )\n            return []\n        quick_trade_details = exchange_constants['quicktrade']\n        # format: [{'type': 'network', 'symbol': 'rune-usdt', 'active': True}, ...]\n        quick_trade_only_pairs = []\n        for pair_details in quick_trade_details:\n            if \"type\" not in pair_details or \"symbol\" not in pair_details:\n                self.logger.error(f\"Ignored invalid quick trade only pair details: {pair_details}\")\n                continue\n            # type=pro means this pair is traded in spot order book markets, otherwise it's a quick trade only pair\n            if pair_details['type'] != \"pro\":\n                market_id = pair_details[\"symbol\"]\n                market = self.client.safe_market(market_id, None, '-')\n                quick_trade_only_pairs.append(\n                    market[trading_enums.ExchangeConstantsMarketStatusColumns.SYMBOL.value]\n                )\n        return quick_trade_only_pairs\n\n    async def _refresh_exchange_fee_tiers(self, all_markets: list[dict]):\n        self.logger.info(f\"Refreshing {self.exchange_manager.exchange_name} fee tiers\")\n        response = await self.client.publicGetTiers()\n        # similar to ccxt's fetch_trading_fees except that we parse all tiers\n        if not response:\n            self.logger.error(\"No fee tiers available\")\n        fees_by_tier = {}\n        for tier, values in response.items():\n            fees = self.client.safe_value(values, 'fees', {})\n            makerFees = self.client.safe_value(fees, 'maker', {})\n            takerFees = self.client.safe_value(fees, 'taker', {})\n            result: dict = {}\n            for market in all_markets:\n                # get symbol, taker and maker fee for each traded pair identified by its id\n                symbol = market[trading_enums.ExchangeConstantsMarketStatusColumns.SYMBOL.value]\n                maker_string = self.client.safe_string(\n                    makerFees, market[trading_enums.ExchangeConstantsMarketStatusColumns.ID.value]\n                )\n                taker_string = self.client.safe_string(\n                    takerFees, market[trading_enums.ExchangeConstantsMarketStatusColumns.ID.value]\n                )\n                if not (maker_string and taker_string):\n                    self.logger.error(\n                        f\"Missing fee details for {symbol} in fetched {self.exchange_manager.exchange_name} fees \"\n                        f\"(using {market[trading_enums.ExchangeConstantsMarketStatusColumns.ID.value]} as market id)\"\n                    )\n                    continue\n                result[symbol] = {\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value:\n                        self.client.parse_number(ccxt.Precise.string_div(maker_string, '100')),\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value:\n                        self.client.parse_number(ccxt.Precise.string_div(taker_string, '100')),\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.FEE_SIDE.value: market.get(\n                        trading_enums.ExchangeConstantsMarketPropertyColumns.FEE_SIDE.value, DEFAULT_FEE_SIDE\n                    )\n                    # don't keep unecessary info\n                    # 'info': fees,\n                    # 'symbol': symbol,\n                    # 'percentage': True,\n                    # 'tierBased': True,\n                }\n            fees_by_tier[tier] = result\n        exchange_name = self.exchange_manager.exchange_name\n        _EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME[exchange_name] = fees_by_tier\n        _REFRESHED_EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME[exchange_name] = True\n        sample = {\n            tier: next(iter(fees.values())) if fees else None\n            for tier, fees in fees_by_tier.items()\n        }\n        fee_pairs = list(fees_by_tier[next(iter(fees_by_tier))]) if fees_by_tier else []\n        self.logger.info(\n            f\"Refreshed {exchange_name} fee tiers. Sample: {sample}. {len(sample)} tiers: {list(sample)} \"\n            f\"over {len(fee_pairs)} pairs: {fee_pairs}. Using fee tiers \"\n            f\"{self._get_fee_tiers(self.exchange_manager.exchange, not self.exchange_manager.is_backtesting).value}.\"\n        )\n\n    @classmethod\n    def simulator_connector_calculate_fees_factory(cls, exchange_name: str, tiers: FeeTiers):\n        # same signature as ExchangeSimulatorConnector.calculate_fees\n        def simulator_connector_calculate_fees(\n            symbol: str, order_type: trading_enums.TraderOrderType,\n            quantity: decimal.Decimal, price: decimal.Decimal, taker_or_maker: str\n        ):\n            # no try/catch: should raise in case fees are not available\n            return cls._calculate_fetched_fees(\n                exchange_name, tiers, symbol, order_type, quantity, price, taker_or_maker\n            )\n        return simulator_connector_calculate_fees\n\n    @classmethod\n    def simulator_connector_get_fees_factory(cls, exchange_name: str, tiers: FeeTiers):\n        # same signature as ExchangeSimulatorConnector.get_fees\n        def simulator_connector_get_fees(symbol):\n            return cls._get_fees(exchange_name, tiers, symbol)\n        return simulator_connector_get_fees\n\n    @classmethod\n    def register_simulator_connector_fee_methods(\n        cls, exchange_name: str, simulator_connector: exchanges.ExchangeSimulatorConnector\n    ):\n        # only called in backtesting\n        # overrides exchange simulator connector calculate_fees and get_fees to use fetched fees instead\n        fee_tiers = cls._get_fee_tiers(None, False)\n        simulator_connector.calculate_fees = cls.simulator_connector_calculate_fees_factory(exchange_name, fee_tiers)\n        simulator_connector.get_fees = cls.simulator_connector_get_fees_factory(exchange_name, fee_tiers)\n\n    def calculate_fees(\n        self, symbol: str, order_type: trading_enums.TraderOrderType,\n        quantity: decimal.Decimal, price: decimal.Decimal, taker_or_maker: str\n    ):\n        # only called in live trading\n        is_real_trading = not self.exchange_manager.is_backtesting  # consider live trading as real to use basic tier\n        try:\n            fee_tiers = self._get_fee_tiers(self.exchange_manager.exchange, is_real_trading)\n            return self._calculate_fetched_fees(\n                self.exchange_manager.exchange_name, fee_tiers,\n                symbol, order_type, quantity, price, taker_or_maker\n            )\n        except errors.MissingFeeDetailsError as err:\n            self.logger.error(f\"Error calculating fees: {err}. Using default ccxt values\")\n            # live trading: can fallback to ccxt default values as the ccxt client exists and is initialized\n            return super().calculate_fees(symbol, order_type, quantity, price, taker_or_maker)\n\n    def get_fees(self, symbol):\n        # only called in live trading\n        try:\n            is_real_trading = not self.exchange_manager.is_backtesting  # consider live trading as real to use basic tier\n            fee_tiers = self._get_fee_tiers(self.exchange_manager.exchange, is_real_trading)\n            return self._get_fees(self.exchange_manager.exchange_name, fee_tiers, symbol)\n        except errors.MissingFeeDetailsError:\n            if _EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME.get(self.exchange_manager.exchange_name):\n                self.logger.error(f\"Missing {self.exchange_manager.exchange_name} {symbol} fee details, using default value\")\n            else:\n                self.logger.warning(f\"Missing all {self.exchange_manager.exchange_name} fee details, using ccxt default values\")\n            market = self.get_market_status(symbol, with_fixer=False)\n            # use default ccxt values\n            return {\n                trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value: market[\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value\n                ],\n                trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value: market[\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value\n                ],\n                trading_enums.ExchangeConstantsMarketPropertyColumns.FEE_SIDE.value: market.get(\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.FEE_SIDE.value, DEFAULT_FEE_SIDE\n                ),\n                trading_enums.ExchangeConstantsMarketPropertyColumns.FEE.value: market.get(\n                    trading_enums.ExchangeConstantsMarketPropertyColumns.FEE.value,\n                    trading_constants.CONFIG_DEFAULT_FEES\n                )\n            }\n\n    @classmethod\n    def _calculate_fetched_fees(\n        cls, exchange_name: str, fee_tiers: FeeTiers, symbol: str, order_type: trading_enums.TraderOrderType,\n        quantity: decimal.Decimal, price: decimal.Decimal, taker_or_maker: str\n    ):\n        # will raise MissingFeeDetailsError if fees details are not available\n        fee_details = cls._get_fetched_fees(exchange_name, fee_tiers, symbol)\n        fee_side = fee_details[trading_enums.ExchangeConstantsMarketPropertyColumns.FEE_SIDE.value]\n        side = exchanges.get_order_side(order_type)\n        # similar as ccxt.Exchange.calculate_fee\n        if fee_side == trading_enums.ExchangeFeeSides.GET.value:\n            # the fee is always in the currency you get\n            use_quote = side == trading_enums.TradeOrderSide.SELL.value\n        elif fee_side == trading_enums.ExchangeFeeSides.GIVE.value:\n            # the fee is always in the currency you give\n            use_quote = side == trading_enums.TradeOrderSide.BUY.value\n        else:\n            # the fee is always in feeSide currency\n            use_quote = fee_side == trading_enums.ExchangeFeeSides.QUOTE.value\n        parsed_symbol = symbols_utils.parse_symbol(symbol)\n        if use_quote:\n            cost = quantity * price\n            fee_currency = parsed_symbol.quote\n        else:\n            cost = quantity\n            fee_currency = parsed_symbol.base\n        fee_rate = decimal.Decimal(str(fee_details[taker_or_maker]))\n        fee_cost = cost * fee_rate\n        return {\n            trading_enums.FeePropertyColumns.TYPE.value: taker_or_maker,\n            trading_enums.FeePropertyColumns.CURRENCY.value: fee_currency,\n            trading_enums.FeePropertyColumns.RATE.value: float(fee_rate),\n            trading_enums.FeePropertyColumns.COST.value: float(fee_cost),\n        }\n\n    @classmethod\n    def _get_fee_tiers(cls, rest_exchange: typing.Optional[exchanges.RestExchange], is_real_trading: bool):\n        if (\n            rest_exchange\n            and isinstance(rest_exchange, hollaex)\n            and (fee_tiers := rest_exchange.get_configured_fee_tiers())\n        ):\n            return fee_tiers\n        # default to basic tier\n        return FeeTiers.BASIC if is_real_trading else FeeTiers.VIP\n\n    @classmethod\n    def _get_fees(cls, exchange_name: str, tiers: FeeTiers, symbol: str):\n        return {\n            ** cls._get_fetched_fees(exchange_name, tiers, symbol),\n            ** {\n                # todo update this if withdrawal fees become relevant\n                trading_enums.ExchangeConstantsMarketPropertyColumns.FEE.value: trading_constants.CONFIG_DEFAULT_FEES\n            }\n        }\n\n    @classmethod\n    def _get_default_fee_symbol(cls, exchange: str):\n        try:\n            exchange_fees = _EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME[exchange]\n            first_fee_tier = next(iter(exchange_fees.values()))\n            return next(iter(first_fee_tier))\n        except (StopIteration, KeyError) as err:\n            raise errors.MissingFeeDetailsError(\n                f\"No available {exchange} fee details {err} ({err.__class__.__name__})\"\n            ) from err\n\n    @classmethod\n    def _get_fetched_fees(cls, exchange: str, tier_to_use: FeeTiers, symbol: str):\n        try:\n            exchange_fees = _EXCHANGE_FEE_TIERS_BY_EXCHANGE_NAME[exchange]\n        except KeyError as err:\n            raise errors.MissingFeeDetailsError(f\"No available {exchange} fee details\") from err\n        try:\n            return exchange_fees[tier_to_use.value][symbol]\n        except KeyError as err:\n            if not exchange_fees:\n                # mssing exchange fees, should not happen\n                raise errors.MissingFeeDetailsError(\n                    f\"Unexpected: missing {exchange} fee details\"\n                ) from err\n            if symbol not in exchange_fees[FeeTiers.BASIC.value]:\n                default_fee_symbol = cls._get_default_fee_symbol(exchange)\n                if symbol == default_fee_symbol:\n                    raise errors.MissingFeeDetailsError(\n                        f\"No available {exchange} {tier_to_use.name} {symbol} fee details\"\n                    ) from err\n                logging.get_logger(cls.__name__).error(\n                    f\"No {symbol} fee tier info on {exchange}: using {default_fee_symbol} fees as default value\"\n                )\n                return cls._get_fetched_fees(exchange, tier_to_use, default_fee_symbol)\n            if tier_to_use.value not in exchange_fees and FeeTiers.BASIC.value in tier_to_use.value:\n                # symbol is in exchange_fees[FeeTiers.BASIC.value] or previous condition would have triggered\n                logging.get_logger(cls.__name__).info(\n                    f\"Falling back on {FeeTiers.BASIC.name} fee tier for {exchange}: no {tier_to_use.name} value\"\n                )\n                return exchange_fees[FeeTiers.BASIC.value][symbol]\n            raise errors.MissingFeeDetailsError(\n                f\"No available {exchange} {tier_to_use.name} {symbol} fee details\"\n            ) from err\n\n\nclass hollaex(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    DEFAULT_CONNECTOR_CLASS = hollaexConnector\n\n    FIX_MARKET_STATUS = True\n\n    BASE_REST_API = \"api.hollaex.com\"\n    REST_KEY = \"rest\"\n    FEE_TIERS_KEY = \"fee_tiers\"\n    HAS_WEBSOCKETS_KEY = \"has_websockets\"\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n\n    IS_SKIPPING_EMPTY_CANDLES_IN_OHLCV_FETCH = True\n\n    # STOP_PRICE is used in ccxt/hollaex instead of default STOP_LOSS_PRICE\n    STOP_LOSS_CREATE_PRICE_PARAM = ccxt_enums.ExchangeOrderCCXTUnifiedParams.STOP_PRICE.value\n    STOP_LOSS_EDIT_PRICE_PARAM = STOP_LOSS_CREATE_PRICE_PARAM\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            # not supported or need custom mechanics with batch orders\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,    # still broken ccxt 4.5.8: stop param is ignored by exchange because it's sent as a string instead of float. Converting it to flaat fails the signature\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n\n    DEFAULT_MAX_LIMIT = 500\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # '\"message\":\"Access denied: Unauthorized Access. This key does not have the right permissions to access this endpoint\"'\n        (\"permissions to access\",),\n    ]\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # {\"message\":\"Access denied: Unauthorized Access.\n        # The IP address you are reaching this endpoint through is not allowed to access this endpoint\"}\n        (\"the ip address\", \"is not allowed\"),\n    ]\n    EXCHANGE_MAX_ORDERS_FOR_MARKET_REACHED_ERRORS: typing.List[typing.Iterable[str]] = [\n        # \"hollaex {\"message\":\"You are only allowed to have maximum 50 active orders per market\"}\"\n        (\"maximum\", \"active orders\", \"per market\"),\n    ]\n\n    def __init__(\n        self, config, exchange_manager, exchange_config_by_exchange: typing.Optional[dict[str, dict]],\n        connector_class=None\n    ):\n        super().__init__(config, exchange_manager, exchange_config_by_exchange, connector_class=connector_class)\n        self.exchange_manager.rest_only = self.exchange_manager.rest_only \\\n            or not self.tentacle_config.get(\n                self.HAS_WEBSOCKETS_KEY, not self.exchange_manager.rest_only\n            )\n\n    def get_adapter_class(self):\n        return HollaexCCXTAdapter\n\n    @classmethod\n    def init_user_inputs_from_class(cls, inputs: dict) -> None:\n        \"\"\"\n        Called at constructor, should define all the exchange's user inputs.\n        \"\"\"\n        cls.CLASS_UI.user_input(\n            cls.REST_KEY, commons_enums.UserInputTypes.TEXT, f\"https://{cls.BASE_REST_API}\", inputs,\n            title=f\"Address of the Hollaex based exchange API (similar to https://{cls.BASE_REST_API})\"\n        )\n        cls.CLASS_UI.user_input(\n            cls.FEE_TIERS_KEY, commons_enums.UserInputTypes.OPTIONS, FeeTiers.BASIC.value, inputs,\n            title=f\"Fee tiers to use for the exchange. Used to predict fees.\",\n            options=[tier.value for tier in FeeTiers]\n        )\n        cls.CLASS_UI.user_input(\n            cls.HAS_WEBSOCKETS_KEY, commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=f\"Use websockets feed. To enable only when websockets are supported by the exchange.\"\n        )\n\n    def get_additional_connector_config(self):\n        return {\n            ccxt_enums.ExchangeColumns.URLS.value: self.get_patched_urls(self.get_api_url())\n        }\n\n    def get_api_url(self):\n        return self.tentacle_config[self.REST_KEY]\n\n    def get_configured_fee_tiers(self) -> typing.Optional[FeeTiers]:\n        if tiers := self.tentacle_config.get(self.FEE_TIERS_KEY):\n            return FeeTiers(tiers)\n        return None\n\n    @classmethod\n    def get_custom_url_config(cls, tentacle_config: dict, exchange_name: str) -> dict:\n        if details := cls.get_exchange_details(tentacle_config, exchange_name):\n            return {\n                ccxt_enums.ExchangeColumns.URLS.value: cls.get_patched_urls(details.api)\n            }\n        return {}\n\n    @classmethod\n    def get_exchange_details(cls, tentacle_config, exchange_name) -> typing.Optional[exchanges.ExchangeDetails]:\n        return None\n\n    @classmethod\n    def get_patched_urls(cls, api_url: str):\n        urls = ccxt.hollaex().urls\n        custom_urls = {\n            ccxt_enums.ExchangeColumns.API.value: {\n                cls.REST_KEY: api_url\n            }\n        }\n        urls.update(custom_urls)\n        return urls\n\n    @classmethod\n    def get_name(cls):\n        return 'hollaex'\n\n    @classmethod\n    def is_configurable(cls):\n        return True\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        signature_identifier = \"api-signature\"\n        return bool(\n            headers\n            and signature_identifier in headers\n        )\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        #  (30/06/2025: Error 1010 - You are only allowed to have maximum 50 active orders per market)\n        return 50\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        with self.connector.error_describer():\n            user_info = await self.connector.client.private_get_user()\n            return user_info[\"id\"]\n\n    async def get_closed_orders(self, symbol: str = None, since: int = None,\n                                limit: int = None, **kwargs: dict) -> list:\n        # get_closed_orders sometimes does not return orders use _get_closed_orders_from_my_recent_trades in this case\n        return (\n            await super().get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs) or\n            await self._get_closed_orders_from_my_recent_trades(\n                symbol=symbol, since=since, limit=limit, **kwargs\n            )\n        )\n\n\nclass HollaexCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_order(self, raw, symbol=None, **kwargs):\n        raw_order_info = raw[ccxt_enums.ExchangePositionCCXTColumns.INFO.value]\n        # average is not supported by ccxt\n        fixed = super().fix_order(raw, symbol=symbol, **kwargs)\n        if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] and \"average\" in raw_order_info:\n            fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = raw_order_info.get(\"average\", 0)\n\n        if fixed[ccxt_enums.ExchangeOrderCCXTColumns.TRIGGER_PRICE.value]:\n            order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n            # todo uncomment when stop loss limit are supported\n            # if fixed[ccxt_enums.ExchangeOrderCCXTColumns.PRICE.value] is None:\n            #     order_type = trading_enums.TradeOrderType.STOP_LOSS.value\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type\n\n        # fees are usually not in order, but if they are, fix them as ccxt is not parsing them\n        self._fix_fees(raw_order_info, fixed)\n        return fixed\n\n    def _fix_fees(self, info, fixed):\n        if (fee_coin := info.get(\"fee_coin\")) and fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.FEE.value):\n            # fee_coin is wrongly overwritten by ccxt as quote currency, used fetched value\n            fixed[trading_enums.ExchangeConstantsOrderColumns.FEE.value][\n                trading_enums.FeePropertyColumns.CURRENCY.value\n            ] = fee_coin.upper()\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/hollaex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"hollaex\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/hollaex/resources/hollaex.md",
    "content": "Hollaex is a basic RestExchange adaptation for Hollaex exchange. \n\nChange the api url to connect to a specific hollaex exchange\n"
  },
  {
    "path": "Trading/Exchange/hollaex/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled/__init__.py",
    "content": "from .hollaex_autofilled_exchange import HollaexAutofilled"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled/config/HollaexAutofilled.json",
    "content": "{\n    \"auto_filled\": {}\n}"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled/hollaex_autofilled_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport cachetools\nimport aiohttp\nimport typing\nimport asyncio\nimport requests.utils\n\nimport octobot_commons.logging as commons_logging\nimport octobot_commons.constants\nimport octobot_commons.html_util as html_util\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.errors as errors\nimport octobot_tentacles_manager.api\nfrom ..hollaex.hollaex_exchange import hollaex\n\n\n_EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL: dict[str, dict] = {}\n# refresh exchange config every day but don't delete outdated info, only replace it with updated ones\n_REFRESHED_EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL : cachetools.TTLCache[str, bool] = cachetools.TTLCache(\n    maxsize=50, ttl=octobot_commons.constants.DAYS_TO_SECONDS\n)\n\nclass HollaexAutofilled(hollaex):\n    HAS_FETCHED_DETAILS = True\n\n    URL_KEY = \"url\"\n    AUTO_FILLED_KEY = \"auto_filled\"\n    WEBSOCKETS_KEY = \"websockets\"\n    KIT_PATH = \"/kit\"\n    V2_KIT_PATH = f\"v2{KIT_PATH}\"\n    MAX_RATE_LIMIT_ATTEMPTS = 60    # fetch over 3 minutes, every 3s (we can't start the bot if the kit request fails)\n    RATE_LIMIT_SLEEP_TIME = 3\n\n    @classmethod\n    def supported_autofill_exchanges(cls, tentacle_config):\n        return list(tentacle_config[cls.AUTO_FILLED_KEY]) if tentacle_config else []\n\n    @classmethod\n    def init_user_inputs_from_class(cls, inputs: dict) -> None:\n        pass\n\n    @classmethod\n    async def get_autofilled_exchange_details(cls, aiohttp_session, tentacle_config, exchange_name):\n        kit_details = await aiohttp_session.get(HollaexAutofilled._get_kit_url(tentacle_config, exchange_name))\n        return HollaexAutofilled._parse_autofilled_exchange_details(\n            tentacle_config, await kit_details.json(), exchange_name\n        )\n\n    def _apply_fetched_details(self, config, exchange_manager):\n        self._apply_config(self.get_exchange_details(self.tentacle_config, exchange_manager.exchange_name))\n\n    @classmethod\n    async def fetch_exchange_config(\n        cls, exchange_config_by_exchange: typing.Optional[dict[str, dict]], exchange_manager\n    ):\n        hollaex_based_exchange_identifier = cls.get_name()\n        if not exchange_config_by_exchange:\n            # no override, try using exchange_manager.tentacles_setup_config\n            exchange_config_by_exchange = {\n                hollaex_based_exchange_identifier: (\n                    octobot_tentacles_manager.api.get_tentacle_config(exchange_manager.tentacles_setup_config, cls)\n                )\n            }\n        if not exchange_config_by_exchange or hollaex_based_exchange_identifier not in exchange_config_by_exchange:\n            raise KeyError(\n                f\"{hollaex_based_exchange_identifier} has to be in exchange_config_by_exchange. \"\n                f\"{exchange_config_by_exchange=}\"\n            )\n        tentacle_config = exchange_config_by_exchange[hollaex_based_exchange_identifier]\n        await cls._cached_fetch_autofilled_config(tentacle_config, exchange_manager.exchange_name)\n\n    @classmethod\n    def _get_user_agent(cls):\n        return requests.utils.default_user_agent()\n\n    @classmethod\n    def _get_headers(cls):\n        return {\n            # same as CCXT\n            'User-Agent': cls._get_user_agent(),\n            \"Accept-Encoding\": \"gzip, deflate\"\n        }\n\n    @classmethod\n    async def _cached_fetch_autofilled_config(cls, tentacle_config, exchange_name) -> dict:\n        try:\n            exchange_kit_url = cls._get_kit_url(tentacle_config, exchange_name)\n        except KeyError:\n            raise errors.NotSupported(f\"{exchange_name} is not supported by {cls.get_name()}\")\n        if exchange_kit_url in _REFRESHED_EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL:\n            return _EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL[exchange_kit_url]\n        commons_logging.get_logger(cls.get_name()).info(\n            f\"Fetching {exchange_name} HollaEx kit from {exchange_kit_url}\"\n        )\n        async with aiohttp.ClientSession(headers=cls._get_headers()) as session:\n            _EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL[exchange_kit_url] = await cls._retry_fetch_when_rate_limit(\n                session, exchange_kit_url\n            )\n            _REFRESHED_EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL[exchange_kit_url] = True\n        return _EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL[exchange_kit_url]\n\n    @classmethod\n    async def _retry_fetch_when_rate_limit(cls, session, url):\n        try:\n            for attempt in range(cls.MAX_RATE_LIMIT_ATTEMPTS):\n                async with session.get(url) as response:\n                    if response.status < 300:\n                        return await response.json()\n                    elif response.status in (403, 429) or \"has banned your IP address\" in (await response.text()):\n                        # rate limit: sleep and retry\n                        commons_logging.get_logger(cls.get_name()).warning(\n                            f\"Error when fetching {url}: {response.status}. Retrying in {cls.RATE_LIMIT_SLEEP_TIME} seconds\"\n                        )\n                        await asyncio.sleep(cls.RATE_LIMIT_SLEEP_TIME)\n                    else:\n                        # unexpected error\n                        response.raise_for_status()\n\n            commons_logging.get_logger(cls.get_name()).error(\n                f\"Error when fetching {url}: {response.status}. Max attempts ({cls.MAX_RATE_LIMIT_ATTEMPTS}) reached. \"\n                f\"Error text: {await response.text()}\"\n            )\n            response.raise_for_status()\n        except aiohttp.ClientResponseError as err:\n            if err.status == 404:\n                raise errors.FailedRequest(f\"{url} returned 404: not found: {err.message}\") from err\n            raise # forward unexpected errors\n        except aiohttp.ClientConnectionError as err:\n            raise errors.NetworkError(\n                f\"Failed to execute request: {err.__class__.__name__}: {html_util.get_html_summary_if_relevant(err)}\"\n            ) from err\n\n    def _supports_autofill(self, exchange_name):\n        try:\n            self._get_kit_url(self.tentacle_config, exchange_name)\n            return True\n        except KeyError:\n            return False\n\n    @classmethod\n    def _get_kit_url(cls, tentacle_config, exchange_name) -> str:\n        exchange_kit_url = HollaexAutofilled._get_autofilled_config(tentacle_config, exchange_name)[cls.URL_KEY]\n        if not exchange_kit_url.endswith(cls.KIT_PATH) and not exchange_kit_url.endswith(\"/\"):\n            exchange_kit_url = f\"{exchange_kit_url}/\"\n        if not exchange_kit_url.endswith(cls.KIT_PATH):\n            exchange_kit_url = f\"{exchange_kit_url}{cls.V2_KIT_PATH}\"\n        return exchange_kit_url\n\n    @classmethod\n    def _has_websocket(cls, tentacle_config, exchange_name):\n        return HollaexAutofilled._get_autofilled_config(tentacle_config, exchange_name).get(cls.WEBSOCKETS_KEY, False)\n\n    @classmethod\n    def _get_autofilled_config(cls, tentacle_config, exchange_name):\n        return tentacle_config[cls.AUTO_FILLED_KEY][exchange_name]\n\n    @classmethod\n    def get_exchange_details(cls, tentacle_config, exchange_name) -> typing.Optional[exchanges.ExchangeDetails]:\n        return cls._parse_autofilled_exchange_details(\n            tentacle_config,\n            _EXCHANGE_REMOTE_CONFIG_BY_EXCHANGE_KIT_URL[\n                cls._get_kit_url(tentacle_config, exchange_name)\n            ],\n            exchange_name\n        )\n\n    @classmethod\n    def _parse_autofilled_exchange_details(cls, tentacle_config, kit_details, exchange_name):\n        \"\"\"\n        use /kit to fill in exchange details\n        format:\n        {\n            \"api_name\": \"BitcoinRD Exchange\",\n            \"black_list_countries\": [],\n            \"captcha\": {},\n            \"color\": {\n                \"Black\": {\n                    \"base_background\": \"#000000\",\n                    ...\n                }\n            },\n            \"defaults\": {\n                \"country\": \"DO\",\n                \"language\": \"es\",\n                \"theme\": \"dark\"\n            },\n            \"description\": \"Primer Exchange 100% Dominicano.\",\n            \"dust\": {\n                \"maker_id\": 1,\n                \"quote\": \"xht\",\n                \"spread\": 0\n            },\n            \"email_verification_required\": true,\n            \"features\": {\n                \"chat\": false,\n                ...\n            },\n            \"icons\": {\n                \"dark\": {\n                    \"DOP_ICON\": \"https://bitholla.s3.ap-northeast-2.amazonaws.com/exchange/bitcoinrdexchange/DOP_ICON__dark___1631209668172.png\",\n                    ...\n                },\n                \"white\": {\n                    \"EXCHANGE_FAV_ICON\": \"https://bitholla.s3.ap-northeast-2.amazonaws.com/exchange/bitcoinrdexchange/EXCHANGE_FAV_ICON__white___1615349464540.png\",\n                    ...\n                }\n            },\n            \"info\": {\n                \"active\": true,\n                \"collateral_level\": \"member\",\n                \"created_at\": \"2021-03-09T14:12:49.012Z\",\n                \"exchange_id\": 1512,\n                \"expiry\": \"2023-08-27T23:59:59.000Z\",\n                \"initialized\": true,\n                \"is_trial\": false,\n                \"name\": \"bitcoinrdexchange\",\n                \"period\": \"year\",\n                \"plan\": \"fiat\",\n                \"status\": true,\n                \"type\": \"Cloud\",\n                \"url\": \"https://api.bitcoinrd.do\",\n                \"user_id\": 3536\n            },\n            \"injected_html\": {\n                \"body\": \"\",\n                \"head\": \"\"\n            },\n            \"injected_values\": [],\n            \"interface\": {},\n            \"links\": {\n                \"api\": \"https://api.bitcoinrd.do\",\n                \"contact\": \"\",\n                \"facebook\": \"\",\n                \"github\": \"\",\n                \"helpdesk\": \"mailto:soporte@bitcoinrd.do\",\n                \"information\": \"\",\n                \"instagram\": \"\",\n                \"linkedin\": \"\",\n                \"privacy\": \"https://bitcoinrd.online/privacy-policy/\",\n                \"referral_label\": \"Powered by BitcoinRD\",\n                \"referral_link\": \"https://bitcoinrd.online/\",\n                \"section_1\": {\n                    \"content\": {\n                        \"instagram\": \"https://www.instagram.com/bitcoinrd/\"\n                    },\n                    \"header\": {\n                        \"column_header_1\": \"RRSS\"\n                    }\n                },\n                \"section_2\": \"\",\n                \"telegram\": \"\",\n                \"terms\": \"https://bitcoinrd.online/terms/\",\n                \"twitter\": \"\",\n                \"website\": \"\",\n                \"whitepaper\": \"\"\n            },\n            \"logo_image\": \"https://bitholla.s3.ap-northeast-2.amazonaws.com/exchange/bitcoinrdexchange/EXCHANGE_LOGO__dark___1615345052424.png\",\n            \"meta\": {\n                \"default_digital_assets_sort\": \"change\",\n                ...\n                },\n                \"versions\": {\n                    \"color\": \"color-1681492596812\",\n                    ...\n                }\n            },\n            \"native_currency\": \"usdt\",\n            \"new_user_is_activated\": true,\n            \"offramp\": {...},\n            \"onramp\": {...},\n            \"setup_completed\": true,\n            \"strings\": {\n                \"en\": { ...}\n            },\n            \"title\": \"\",\n            \"user_meta\": {},\n            \"user_payments\": {},\n            \"valid_languages\": \"en,es,fr\"\n        }\n        \"\"\"\n        return exchanges.ExchangeDetails(\n            exchange_name,\n            kit_details.get(\"api_name\", exchange_name),\n            kit_details[\"links\"].get(\"referral_link\", \"\"),\n            kit_details[\"info\"][\"url\"], # required (API url)\n            kit_details.get(\"logo_image\", \"\"),\n            HollaexAutofilled._has_websocket(\n                tentacle_config,\n                exchange_name\n            )\n        )\n\n    def _apply_config(self, autofilled_exchange_details: exchanges.ExchangeDetails):\n        self.logger = commons_logging.get_logger(autofilled_exchange_details.name)\n        self.tentacle_config[self.REST_KEY] = autofilled_exchange_details.api\n        self.tentacle_config[self.HAS_WEBSOCKETS_KEY] = autofilled_exchange_details.has_websocket\n\n    @classmethod\n    def is_supporting_sandbox(cls) -> bool:\n        return False\n\n    @classmethod\n    def get_rest_name(cls, exchange_manager):\n        return hollaex.get_name()\n\n    def get_associated_websocket_exchange_name(self):\n        return self.get_name()\n\n    @classmethod\n    def get_name(cls):\n        return cls.__name__\n"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"HollaexAutofilled\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled/resources/hollaex_autofilled.md",
    "content": "Basic RestExchange adaptation for auto filled exchange using HollaEx"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled_websocket_feed/__init__.py",
    "content": "from .hollaex_autofilled_websocket import HollaexAutofilledCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled_websocket_feed/hollaex_autofilled_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom ..hollaex_autofilled.hollaex_autofilled_exchange import HollaexAutofilled\nfrom ..hollaex_websocket_feed.hollaex_websocket import HollaexCCXTWebsocketConnector\n\n\nclass HollaexAutofilledCCXTWebsocketConnector(HollaexCCXTWebsocketConnector):\n    def _get_logger_name(self):\n        return f\"WebSocket - {self._get_visible_name()}\"\n\n    def _get_visible_name(self):\n        return self.exchange_manager.exchange_name\n\n    @classmethod\n    def get_name(cls):\n        return HollaexAutofilled.get_name()\n\n    def get_feed_name(self):\n        return HollaexCCXTWebsocketConnector.get_name()\n"
  },
  {
    "path": "Trading/Exchange/hollaex_autofilled_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"HollaexAutofilledCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/hollaex_websocket_feed/__init__.py",
    "content": "from .hollaex_websocket import HollaexCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/hollaex_websocket_feed/hollaex_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport ccxt.pro as ccxt_pro\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nfrom ..hollaex.hollaex_exchange import hollaex\n\n\nclass HollaexCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    BASE_WS_API = f\"{hollaex.BASE_REST_API}/stream\"\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: Feeds.UNSUPPORTED.value,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,\n        Feeds.TICKER: Feeds.UNSUPPORTED.value,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,\n    }\n\n    def _create_client(self, force_unauth=False):\n        if not self.additional_config:\n            additional_connector_config = self.exchange_manager.exchange.get_additional_connector_config()\n            try:\n                self._update_urls(additional_connector_config)\n                # use rest exchange additional config if any\n                self.additional_config = additional_connector_config\n            except KeyError as err:\n                self.logger.error(f\"Error when updating exchange url: {err}\")\n        super()._create_client()\n\n    def _update_urls(self, additional_connector_config):\n        rest_url = additional_connector_config[ccxt_enums.ExchangeColumns.URLS.value][\n            ccxt_enums.ExchangeColumns.API.value\n        ][ccxt_enums.ExchangeColumns.REST.value]\n        if hollaex.BASE_REST_API not in rest_url:\n            current_ws_url = ccxt_pro.hollaex().describe()[ccxt_enums.ExchangeColumns.URLS.value][\n                ccxt_enums.ExchangeColumns.API.value\n            ][ccxt_enums.ExchangeColumns.WEBSOCKET.value]\n            custom_url = rest_url.split(\"https://\")[1]\n            additional_connector_config[ccxt_enums.ExchangeColumns.URLS.value][\n                ccxt_enums.ExchangeColumns.API.value\n            ][ccxt_enums.ExchangeColumns.WEBSOCKET.value] = current_ws_url.replace(hollaex.BASE_REST_API, custom_url)\n\n    @classmethod\n    def get_name(cls):\n        return hollaex.get_name()\n"
  },
  {
    "path": "Trading/Exchange/hollaex_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"HollaexCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/htx/__init__.py",
    "content": "from .htx_exchange import Htx"
  },
  {
    "path": "Trading/Exchange/htx/htx_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\n\n\nclass Htx(exchanges.RestExchange):\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    @classmethod\n    def get_name(cls):\n        return 'htx'\n\n    def get_adapter_class(self):\n        return HtxCCXTAdapter\n\n    def get_additional_connector_config(self):\n        # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n        # (price should not be sent to market orders). Only used for buy market orders\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"createMarketBuyOrderRequiresPrice\": False  # disable quote conversion\n            }\n        }\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = 500, **kwargs: dict):\n        history_param = \"useHistoricalEndpointForSpot\"\n        if limit and history_param not in kwargs:\n            # required to handle limits\n            kwargs[history_param] = False\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n\n\nclass HtxCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        self.adapt_amount_from_filled_or_cost(fixed)\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/htx/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Htx\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/htx/resources/htx.md",
    "content": "HTX is a basic RestExchange adaptation for HTX exchange. \n"
  },
  {
    "path": "Trading/Exchange/htx_websocket_feed/__init__.py",
    "content": "from .htx_websocket import HtxCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/htx_websocket_feed/htx_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.htx.htx_exchange as htx_exchange\n\n\nclass HtxCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return htx_exchange.Htx.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return htx_exchange.HtxCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/htx_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"HtxCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/huobi/__init__.py",
    "content": "from .huobi_exchange import Huobi"
  },
  {
    "path": "Trading/Exchange/huobi/huobi_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.htx.htx_exchange as htx_exchange\n\n\nclass Huobi(htx_exchange.Htx):\n    # kept for legacy support (users using huobi instead of HTX)\n    @classmethod\n    def get_name(cls):\n        return 'huobi'\n"
  },
  {
    "path": "Trading/Exchange/huobi/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Huobi\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/huobi/resources/huobi.md",
    "content": "Huobi is a basic RestExchange adaptation for Huobi exchange. \n"
  },
  {
    "path": "Trading/Exchange/huobi_websocket_feed/__init__.py",
    "content": "from .huobi_websocket import HuobiCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/huobi_websocket_feed/huobi_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.huobi.huobi_exchange as huobi_exchange\nimport tentacles.Trading.Exchange.htx_websocket_feed.htx_websocket as htx_websocket\n\n\nclass HuobiCCXTWebsocketConnector(htx_websocket.HtxCCXTWebsocketConnector):\n    # kept for legacy support (users using huobi instead of HTX)\n    @classmethod\n    def get_name(cls):\n        return huobi_exchange.Huobi.get_name()\n\n"
  },
  {
    "path": "Trading/Exchange/huobi_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"HuobiCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/huobi_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/huobi_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...huobi_websocket_feed import HuobiCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    exchange_manager_instance = await exchanges_test_tools.create_test_exchange_manager(\n        config=config, exchange_name=HuobiCryptofeedWebsocketConnector.get_name())\n\n    await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n        websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n        websocket_connector_class=HuobiCryptofeedWebsocketConnector,\n        exchange_manager=exchange_manager_instance,\n        config=config,\n        symbols=[\"BTC/USDT\", \"ETH/USDT\"],\n        time_frames=[commons_enums.TimeFrames.ONE_MINUTE, commons_enums.TimeFrames.ONE_HOUR],\n        expected_pushed_channels={\n            channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value,\n        },\n        time_before_assert=20\n    )\n    await exchanges_test_tools.stop_test_exchange_manager(exchange_manager_instance)\n"
  },
  {
    "path": "Trading/Exchange/hyperliquid/__init__.py",
    "content": "from .hyperliquid_exchange import Hyperliquid"
  },
  {
    "path": "Trading/Exchange/hyperliquid/hyperliquid_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\n\n\nclass HyperliquidConnector(exchanges.CCXTConnector):\n\n    def _client_factory(\n        self,\n        force_unauth,\n        keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None\n    ) -> tuple:\n        return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)\n\n    def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:\n        # use api key and secret as wallet address and private key\n        creds.wallet_address = creds.api_key\n        creds.private_key = creds.secret\n        creds.api_key = creds.secret = None\n        return creds\n\n\nclass Hyperliquid(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    DEFAULT_CONNECTOR_CLASS = HyperliquidConnector\n\n    FIX_MARKET_STATUS = True\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    # should be fetched using recent trades.\n\n    @classmethod\n    def get_name(cls):\n        return 'hyperliquid'\n\n    def get_adapter_class(self):\n        return HyperLiquidCCXTAdapter\n\n    def get_additional_connector_config(self):\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"fetchMarkets\": {\n                    \"types\": [\"spot\"],  # only hyperliquid spot markets are supported\n                }\n            }\n        }\n\n\nclass HyperLiquidCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n\n    def fix_market_status(self, raw, remove_price_limits=False, **kwargs):\n        fixed = super().fix_market_status(raw, remove_price_limits=remove_price_limits, **kwargs)\n        if not fixed:\n            return fixed\n        # hyperliquid min cost should be increased by 10% (a few cents above min cost is refused)\n        limits = fixed[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value]\n        limits[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST.value][\n            trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value\n        ] = limits[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST.value][\n            trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value\n        ] * 1.1\n\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/hyperliquid/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Hyperliquid\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/hyperliquid/resources/hyperliquid.md",
    "content": "Hyperliquid is a basic RestExchange adaptation for Hyperliquid exchange. \n"
  },
  {
    "path": "Trading/Exchange/hyperliquid/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/hyperliquid_websocket_feed/__init__.py",
    "content": "from .hyperliquid_websocket import HyperliquidCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/hyperliquid_websocket_feed/hyperliquid_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.hyperliquid.hyperliquid_exchange as hyperliquid_exchange\n\n\nclass HyperliquidCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    USE_REST_CONNECTOR_ADDITIONAL_CONFIG = True\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return hyperliquid_exchange.Hyperliquid.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return hyperliquid_exchange.HyperLiquidCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/hyperliquid_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"HyperliquidCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/kraken/__init__.py",
    "content": "from .kraken_exchange import Kraken"
  },
  {
    "path": "Trading/Exchange/kraken/kraken_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.errors\nimport octobot_trading.enums as trading_enums\n\n\nclass Kraken(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    RECENT_TRADE_FIXED_LIMIT = 1000\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    def __init__(\n        self, config, exchange_manager, exchange_config_by_exchange: typing.Optional[dict[str, dict]],\n        connector_class=None\n    ):\n        super().__init__(config, exchange_manager, exchange_config_by_exchange, connector_class=connector_class)\n        self.logger.error(\"Kraken is not providing free and used data for account balance. \"\n                          \"OctoBot wont be able to manage a real portfolio correctly.\")\n\n    @classmethod\n    def get_name(cls):\n        return 'kraken'\n\n    def get_adapter_class(self):\n        return KrakenCCXTAdapter\n\n    async def get_recent_trades(self, symbol, limit=RECENT_TRADE_FIXED_LIMIT, **kwargs):\n        if limit is not None and limit != self.RECENT_TRADE_FIXED_LIMIT:\n            self.logger.debug(f\"Trying to get_recent_trades with limit != {self.RECENT_TRADE_FIXED_LIMIT} : ({limit})\")\n            limit = self.RECENT_TRADE_FIXED_LIMIT\n        return await super().get_recent_trades(symbol=symbol, limit=limit, **kwargs)\n\n    async def get_order_book(self, symbol, limit=5, **kwargs):\n        # suggestion from https://github.com/ccxt/ccxt/issues/8135#issuecomment-748520283\n        try:\n            return await self.connector.client.fetch_l2_order_book(symbol, limit=limit, params=kwargs)\n        except Exception as e:\n            raise octobot_trading.errors.FailedRequest(f\"Failed to get_order_book {e}\")\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict):\n        # ohlcv limit is not working as expected, limit is doing [:-limit] but we want [-limit:]\n        candles = await super().get_symbol_prices(symbol=symbol, time_frame=time_frame, limit=limit, **kwargs)\n        if limit:\n            return candles[-limit:]\n        return candles\n\n\nclass KrakenCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/kraken/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Kraken\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/kraken/resources/kraken.md",
    "content": "Kraken is a basic RestExchange adaptation for Kraken exchange. \n"
  },
  {
    "path": "Trading/Exchange/kraken/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/kraken_websocket_feed/__init__.py",
    "content": "from .kraken_websocket import KrakenCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/kraken_websocket_feed/kraken_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.kraken.kraken_exchange as kraken_exchange\n\n\nclass KrakenCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return kraken_exchange.Kraken.get_name()\n"
  },
  {
    "path": "Trading/Exchange/kraken_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"KrakenCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/kraken_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/kraken_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...kraken_websocket_feed import KrakenCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    exchange_manager_instance = await exchanges_test_tools.create_test_exchange_manager(\n        config=config, exchange_name=KrakenCryptofeedWebsocketConnector.get_name())\n\n    await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n        websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n        websocket_connector_class=KrakenCryptofeedWebsocketConnector,\n        exchange_manager=exchange_manager_instance,\n        config=config,\n        symbols=[\"BTC/USDT\", \"ETH/USDT\"],\n        time_frames=[commons_enums.TimeFrames.ONE_MINUTE, commons_enums.TimeFrames.ONE_HOUR],\n        expected_pushed_channels={\n            channels_name.OctoBotTradingChannelsName.RECENT_TRADES_CHANNEL.value,\n            channels_name.OctoBotTradingChannelsName.TICKER_CHANNEL.value,\n            channels_name.OctoBotTradingChannelsName.OHLCV_CHANNEL.value,\n            channels_name.OctoBotTradingChannelsName.KLINE_CHANNEL.value,\n        },\n        time_before_assert=20\n    )\n    await exchanges_test_tools.stop_test_exchange_manager(exchange_manager_instance)\n"
  },
  {
    "path": "Trading/Exchange/kucoin/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom .kucoin_exchange import Kucoin\n"
  },
  {
    "path": "Trading/Exchange/kucoin/kucoin_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Private-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport time\nimport decimal\nimport typing\nimport ccxt\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.logging as logging\nimport octobot_trading.errors\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_connector as ccxt_connector\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_client_util as ccxt_client_util\nimport octobot_trading.constants as constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport octobot.community\n\n\n_CACHED_CONFIRMED_FEES_BY_SYMBOL = {}\n\n\ndef _kucoin_retrier(f):\n    async def kucoin_retrier_wrapper(*args, **kwargs):\n        last_error = None\n        for i in range(0, Kucoin.FAKE_DDOS_ERROR_INSTANT_RETRY_COUNT):\n            try:\n                return await f(*args, **kwargs)\n            except (octobot_trading.errors.FailedRequest, ccxt.ExchangeError) as err:\n                last_error = err\n                rest_exchange = args[0]  # self\n                if (rest_exchange.connector is not None) and \\\n                    rest_exchange.connector.client.last_http_response and \\\n                    Kucoin.INSTANT_RETRY_ERROR_CODE in rest_exchange.connector.client.last_http_response:\n                    # should retry instantly, error on kucoin side\n                    # see https://github.com/Drakkar-Software/OctoBot/issues/2000\n                    logging.get_logger(Kucoin.get_name()).debug(\n                        f\"{Kucoin.INSTANT_RETRY_ERROR_CODE} error on {f.__name__}(args={args[1:]} kwargs={kwargs}) \"\n                        f\"request, retrying now. Attempt {i+1} / {Kucoin.FAKE_DDOS_ERROR_INSTANT_RETRY_COUNT}, \"\n                        f\"error: {err} ({last_error.__class__.__name__}).\"\n                    )\n                else:\n                    raise\n        last_error = last_error or RuntimeError(\"Unknown Kucoin error\")  # to be able to \"raise from\" in next line\n        raise octobot_trading.errors.FailedRequest(\n            f\"Failed Kucoin request after {Kucoin.FAKE_DDOS_ERROR_INSTANT_RETRY_COUNT} \"\n            f\"retries on {f.__name__}(args={args[1:]} kwargs={kwargs}) due \"\n            f\"to {Kucoin.INSTANT_RETRY_ERROR_CODE} error code. \"\n            f\"Last error: {last_error} ({last_error.__class__.__name__})\"\n        ) from last_error\n    return kucoin_retrier_wrapper\n\n\nclass KucoinConnector(ccxt_connector.CCXTConnector):\n\n    @_kucoin_retrier\n    async def _load_markets(\n        self, \n        client, \n        reload: bool, \n        market_filter: typing.Optional[typing.Callable[[dict], bool]] = None\n    ):\n        # override for retrier\n        await self._filtered_if_necessary_load_markets(client, reload, market_filter)\n        # sometimes market fees are missing because they are fetched from all tickers \n        # and all ticker can miss symbols on kucoin\n        if client.markets:\n            ccxt_client_util.fix_client_missing_markets_fees(client, reload, _CACHED_CONFIRMED_FEES_BY_SYMBOL)\n\nclass Kucoin(exchanges.RestExchange):\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n    ADAPT_MARKET_STATUS_FOR_CONTRACT_SIZE = True\n    # Set True when get_open_order() can return outdated orders (cancelled or not yet created)\n    CAN_HAVE_DELAYED_OPEN_ORDERS = True\n    # Set True when get_cancelled_order() can return outdated open orders\n    CAN_HAVE_DELAYED_CANCELLED_ORDERS = True\n    DEFAULT_CONNECTOR_CLASS = KucoinConnector\n    # set True when even loading markets can make auth calls when creds are set\n    CAN_MAKE_AUTHENTICATED_REQUESTS_WHEN_LOADING_MARKETS = True\n\n    FAKE_DDOS_ERROR_INSTANT_RETRY_COUNT = 5\n    INSTANT_RETRY_ERROR_CODE = \"429000\"\n    FUTURES_CCXT_CLASS_NAME = \"kucoinfutures\"\n    MAX_INCREASED_POSITION_QUANTITY_MULTIPLIER = decimal.Decimal(\"0.95\")\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n\n    # set True when fetch_tickers can sometimes miss symbols. In this case, the connector will try to fix it\n    CAN_MISS_TICKERS_IN_ALL_TICKERS = True\n\n    # set True when get_positions() is not returning empty positions and should use get_position() instead\n    REQUIRES_SYMBOL_FOR_EMPTY_POSITION = True\n\n    # set False when the exchange refuses to change margin type when an associated position is open\n    SUPPORTS_SET_MARGIN_TYPE_ON_OPEN_POSITIONS = False\n\n    # get_my_recent_trades only covers the last 24h on kucoin\n    ALLOW_TRADES_FROM_CLOSED_ORDERS = True  # set True when get_my_recent_trades should use get_closed_orders\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on futures\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,  # supported\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on spot\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n    # text content of errors due to api key permissions issues\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'kucoinfutures Access denied, require more permission'\n        (\"require more permission\",),\n    ]\n    # text content of errors due to account compliancy issues\n    EXCHANGE_COMPLIANCY_ERRORS: typing.List[typing.Iterable[str]] = [\n        # kucoin {\"msg\":\"Unfortunately, trading is currently unavailable in your location due to country, region, or IP restrictions.\",\"code\":\"600004\"}\n        (\"trading is currently unavailable in your location\",),\n    ]\n    # text content of errors due to orders not found errors\n    EXCHANGE_ORDER_NOT_FOUND_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'kucoin The order does not exist.'\n        (\"order does not exist\",),\n    ]\n    # text content of errors due to a closed position on the exchange. Relevant for reduce-only orders\n    EXCHANGE_CLOSED_POSITION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'kucoinfutures No open positions to close.'\n        (\"no open positions to close\", )\n    ]\n    # text content of errors due to an order that would immediately trigger if created. Relevant for stop losses\n    EXCHANGE_ORDER_IMMEDIATELY_TRIGGER_ERRORS: typing.List[typing.Iterable[str]] = [\n        # doesn't seem to happen on kucoin\n    ]\n    # text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)\n    EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [\n        ('order cannot be canceled', ),\n        ('order_not_exist_or_not_allow_to_cancel', )\n    ]\n    # text content of errors due to unhandled IP white list issues\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # \"kucoinfutures Invalid request ip, the current clientIp is:e3b:e3b:e3b:e3b:e3b:e3b:e3b:e3b\"\n        (\"invalid request ip\",),\n    ]\n    # set when the exchange can allow users to pay fees in a custom currency (ex: BNB on binance)\n    LOCAL_FEES_CURRENCIES: typing.List[str] = [\"KCS\"]\n\n    DEFAULT_BALANCE_CURRENCIES_TO_FETCH = [\"USDT\"]\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n\n    @classmethod\n    def get_name(cls):\n        return 'kucoin'\n\n    @classmethod\n    def get_rest_name(cls, exchange_manager):\n        if exchange_manager.is_future:\n            return cls.FUTURES_CCXT_CLASS_NAME\n        return cls.get_name()\n\n    def get_adapter_class(self):\n        return KucoinCCXTAdapter\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def supports_api_leverage_update(self, symbol: str) -> bool:\n        \"\"\"\n        Override if necessary\n        :param symbol:\n        :return:\n        \"\"\"\n        if super().supports_api_leverage_update(symbol):\n            # set leverage is only supported on cross positions\n            # https://www.kucoin.com/docs/rest/futures-trading/positions/modify-cross-margin-leverage\n            try:\n                return self.exchange_manager.exchange_personal_data.positions_manager.get_symbol_position_margin_type(\n                    symbol\n                ) is trading_enums.MarginType.CROSS\n            except ValueError as err:\n                self.logger.exception(f\"Failed to get {symbol} position margin type: {err}\")\n        return False\n\n    async def set_symbol_leverage(self, symbol: str, leverage: float, **kwargs):\n        params = kwargs or {}\n        if self.exchange_manager.is_future:\n            # add marginMode param as required by ccxt\n            self._set_margin_mode_param_if_necessary(symbol, params, lower=True)\n        return await super().set_symbol_leverage(symbol, leverage, **params)\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        # from\n        #   https://www.kucoin.com/docs-new/rest/futures-trading/orders/add-order\n        #   https://www.kucoin.com/docs-new/rest/spot-trading/orders/add-order\n        # should be 100 to 200 but use 100 to be sure\n        return 100\n\n    def supports_native_edit_order(self, order_type: trading_enums.TraderOrderType) -> bool:\n        # return False when default edit_order can't be used and order should always be canceled and recreated instead\n        # only working on HF orders\n        return False\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        # It is currently impossible to fetch subaccounts account id, use a constant value to identify it.\n        # updated: 21/05/2024\n        try:\n            with self.connector.error_describer():\n                account_id = None\n                subaccount_id = None\n                sub_accounts = await self.connector.client.private_get_sub_accounts()\n                accounts = sub_accounts.get(\"data\", {}).get(\"items\", {})\n                has_subaccounts = bool(accounts)\n                if has_subaccounts:\n                    if len(accounts) == 1:\n                        # only 1 account: use its id or name\n                        account = accounts[0]\n                        # try using subUserId if available\n                        # 'ex subUserId: 65d41ea409407d000160cc17 subName: octobot1'\n                        account_id = account.get(\"subUserId\") or account[\"subName\"]\n                    else:\n                        # more than 1 account: consider other accounts\n                        for account in accounts:\n                            if account[\"subUserId\"]:\n                                subaccount_id = account[\"subName\"]\n                            else:\n                                # only subaccounts have a subUserId: if this condition is True, we are on the main account\n                                account_id = account[\"subName\"]\n                    if account_id and self.exchange_manager.is_future:\n                        account_id = octobot.community.to_community_exchange_internal_name(\n                            account_id, commons_constants.CONFIG_EXCHANGE_FUTURE\n                        )\n                if subaccount_id:\n                    # there is at least a subaccount: ensure the current account is the main account as there is no way\n                    # to know the id of the current account (only a list of existing accounts)\n                    subaccount_api_key_details = await self.connector.client.private_get_sub_api_key(\n                        {\"subName\": subaccount_id}\n                    )\n                    if \"data\" not in subaccount_api_key_details or \"msg\" in subaccount_api_key_details:\n                        # subaccounts can't fetch other accounts data, if this is False, we are on a subaccount\n                        self.logger.error(\n                            f\"kucoin api changed: it is now possible to call private_get_sub_accounts on subaccounts. \"\n                            f\"kucoin get_account_id has to be updated. \"\n                            f\"sub_accounts={sub_accounts} subaccount_api_key_details={subaccount_api_key_details}\"\n                        )\n                        return constants.DEFAULT_ACCOUNT_ID\n                if has_subaccounts and account_id is None:\n                    self.logger.error(\n                        f\"kucoin api changed: can't fetch master account account_id. \"\n                        f\"kucoin get_account_id has to be updated.\"\n                        f\"sub_accounts={sub_accounts}\"\n                    )\n                    account_id = constants.DEFAULT_ACCOUNT_ID\n                # we are on the master account\n                return account_id or constants.DEFAULT_ACCOUNT_ID\n        except ccxt.ExchangeError as err:\n            # ExchangeError('kucoin This user is not a master user')\n            if \"not a master user\" not in str(err):\n                self.logger.error(f\"kucoin api changed: subaccount error on account id is now: '{err}' \"\n                                  f\"instead of 'kucoin This user is not a master user'\")\n            # raised when calling this endpoint with a subaccount\n            return constants.DEFAULT_SUBACCOUNT_ID\n\n    def get_market_status(self, symbol, price_example=None, with_fixer=True):\n        \"\"\"\n        local override to take \"minFunds\" into account\n        \"minFunds\tthe minimum spot and margin trading amounts\" https://docs.kucoin.com/#get-symbols-list\n        \"\"\"\n        market_status = super().get_market_status(symbol, price_example=price_example, with_fixer=with_fixer)\n        min_funds = market_status.get(ccxt_constants.CCXT_INFO, {}).get(\"minFunds\")\n        if min_funds is not None:\n            # should only be for spot and margin, use it if available anyway\n            limit_costs = market_status[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value][\n                trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST.value\n            ]\n            # use max (most restrictive) value\n            limit_costs[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value] = max(\n                limit_costs[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS_COST_MIN.value],\n                float(min_funds)\n            )\n        return market_status\n\n    @_kucoin_retrier\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = 200, **kwargs: dict):\n        if \"since\" in kwargs:\n            # prevent ccxt from fillings the end param (not working when trying to get the 1st candle times)\n            kwargs[\"to\"] = int(time.time() * commons_constants.MSECONDS_TO_SECONDS)\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n\n    @_kucoin_retrier\n    async def get_recent_trades(self, symbol, limit=50, **kwargs):\n        # on ccxt kucoin recent trades are received in reverse order from exchange and therefore should never be\n        # filtered by limit before reversing (or most recent trades are lost)\n        recent_trades = await super().get_recent_trades(symbol, limit=None, **kwargs)\n        return recent_trades[::-1][:limit] if recent_trades else []\n\n    @_kucoin_retrier\n    async def get_order_book(self, symbol, limit=20, **kwargs):\n        # override default limit to be kucoin complient\n        return await super().get_order_book(symbol, limit=limit, **kwargs)\n\n    @_kucoin_retrier\n    async def get_price_ticker(self, symbol: str, **kwargs: dict) -> typing.Optional[dict]:\n        return await super().get_price_ticker(symbol, **kwargs)\n\n    @_kucoin_retrier\n    async def get_all_currencies_price_ticker(self, **kwargs: dict) -> typing.Optional[dict[str, dict]]:\n        return await super().get_all_currencies_price_ticker(**kwargs)\n\n    def should_log_on_ddos_exception(self, exception) -> bool:\n        \"\"\"\n        Override when necessary\n        \"\"\"\n        return Kucoin.INSTANT_RETRY_ERROR_CODE not in str(exception)\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        signature_identifier = \"KC-API-SIGN\"\n        return bool(\n            headers\n            and signature_identifier in headers\n        )\n\n    def get_order_additional_params(self, order) -> dict:\n        params = {}\n        if self.exchange_manager.is_future:\n            contract = self.exchange_manager.exchange.get_pair_future_contract(order.symbol)\n            params[\"leverage\"] = float(contract.current_leverage)\n            params[\"reduceOnly\"] = order.reduce_only\n            params[\"closeOrder\"] = order.close_position\n        return params\n\n    async def _update_balance(self, balance, currency, **kwargs):\n        balance.update(await super().get_balance(code=currency, **kwargs))\n\n    @_kucoin_retrier\n    async def get_balance(self, **kwargs: dict):\n        balance = {}\n        if self.exchange_manager.is_future:\n            # on futures, balance has to be fetched per currency\n            # use gather to fetch everything at once (and not allow other requests to get in between)\n            currencies = self.exchange_manager.exchange_config.get_all_traded_currencies()\n            if not currencies:\n                currencies = self.DEFAULT_BALANCE_CURRENCIES_TO_FETCH\n                self.logger.warning(\n                    f\"Can't fetch balance on {self.exchange_manager.exchange_name} futures when no traded currencies \"\n                    f\"are set, fetching {currencies[0]} balance instead\"\n                )\n            await asyncio.gather(*(\n                self._update_balance(balance, currency, **kwargs)\n                for currency in currencies\n            ))\n            return balance\n        return await super().get_balance(**kwargs)\n\n    def fetch_stop_order_in_different_request(self, symbol: str) -> bool:\n        # Override in tentacles when stop orders need to be fetched in a separate request from CCXT\n        # Kucoin uses the algo orders endpoint for all stop orders\n        return True\n\n    @_kucoin_retrier\n    async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        if limit is None:\n            # default is 50, The maximum cannot exceed 1000\n            # https://www.kucoin.com/docs/rest/futures-trading/orders/get-order-list\n            limit = 200\n        return await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)\n\n    @_kucoin_retrier\n    async def get_order(\n        self,\n        exchange_order_id: str,\n        symbol: typing.Optional[str] = None,\n        order_type: typing.Optional[trading_enums.TraderOrderType] = None,\n        **kwargs: dict\n    ) -> dict:\n        return await super().get_order(exchange_order_id, symbol=symbol, order_type=order_type, **kwargs)\n\n    async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,\n                           price: decimal.Decimal = None, stop_price: decimal.Decimal = None,\n                           side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None,\n                           reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]:\n        if self.exchange_manager.is_future:\n            params = params or {}\n            self._set_margin_mode_param_if_necessary(symbol, params)\n        return await super().create_order(order_type, symbol, quantity,\n                                          price=price, stop_price=stop_price,\n                                          side=side, current_price=current_price,\n                                          reduce_only=reduce_only, params=params)\n\n    async def edit_order(self, exchange_order_id: str, order_type: trading_enums.TraderOrderType, symbol: str,\n                         quantity: decimal.Decimal, price: decimal.Decimal,\n                         stop_price: decimal.Decimal = None, side: trading_enums.TradeOrderSide = None,\n                         current_price: decimal.Decimal = None,\n                         params: dict = None):\n        if self.exchange_manager.is_future:\n            params = params or {}\n            self._set_margin_mode_param_if_necessary(symbol, params)\n        return await super().edit_order(\n            exchange_order_id, order_type, symbol, quantity, price, stop_price=stop_price,\n            side=side, current_price=current_price, params=params\n        )\n\n    def _set_margin_mode_param_if_necessary(self, symbol, params, lower=False):\n        try:\n            # \"marginMode\": \"ISOLATED\" // Added field for margin mode: ISOLATED, CROSS, default: ISOLATED\n            # from https://www.kucoin.com/docs/rest/futures-trading/orders/place-order\n            if (\n                KucoinCCXTAdapter.KUCOIN_MARGIN_MODE not in params and\n                self.exchange_manager.exchange_personal_data.positions_manager.get_symbol_position_margin_type(\n                    symbol\n                ) is trading_enums.MarginType.CROSS\n            ):\n                params[KucoinCCXTAdapter.KUCOIN_MARGIN_MODE] = \"cross\" if lower else \"CROSS\"\n        except ValueError as err:\n            self.logger.error(f\"Impossible to add {KucoinCCXTAdapter.KUCOIN_MARGIN_MODE} to order: {err}\")\n\n    @_kucoin_retrier\n    async def cancel_order(\n        self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict\n    ) -> trading_enums.OrderStatus:\n        return await super().cancel_order(exchange_order_id, symbol, order_type, **kwargs)\n\n    # add retried to _create_order_with_retry to avoid catching error in self._order_operation context manager\n    @_kucoin_retrier\n    async def _create_order_with_retry(self, order_type, symbol, quantity: decimal.Decimal,\n                                       price: decimal.Decimal, stop_price: decimal.Decimal,\n                                       side: trading_enums.TradeOrderSide,\n                                       current_price: decimal.Decimal,\n                                       reduce_only: bool, params) -> dict:\n        return await super()._create_order_with_retry(\n            order_type=order_type, symbol=symbol, quantity=quantity, price=price,\n            stop_price=stop_price, side=side, current_price=current_price,\n            reduce_only=reduce_only, params=params\n        )\n\n    @_kucoin_retrier\n    async def get_my_recent_trades(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:\n        return await super().get_my_recent_trades(symbol=symbol, since=since, limit=limit, **kwargs)\n\n    @_kucoin_retrier\n    async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: dict):\n        \"\"\"\n        Set the symbol margin type\n        :param symbol: the symbol\n        :param isolated: when False, margin type is cross, else it's isolated\n        :return: the update result\n        \"\"\"\n        try:\n            return await super().set_symbol_margin_type(symbol, isolated, **kwargs)\n        except ccxt.errors.ExchangeError as err:\n            if \"Please close or cancel them\" in str(err):\n                if self.SUPPORTS_SET_MARGIN_TYPE_ON_OPEN_POSITIONS:\n                    raise\n                else:\n                    raise trading_errors.NotSupported(f\"set_symbol_margin_type is not supported on open positions\")\n            raise\n\n    async def get_position(self, symbol: str, **kwargs: dict) -> dict:\n        \"\"\"\n        Get the current user symbol position list\n        :param symbol: the position symbol\n        :return: the user symbol position list\n        \"\"\"\n\n        # todo remove when supported by ccxt\n        async def fetch_position(client, symbol, params={}):\n            market = client.market(symbol)\n            market_id = market['id']\n            request = {\n                'symbol': market_id,\n            }\n            response = await client.futuresPrivateGetPosition(request)\n            #\n            #    {\n            #        \"code\": \"200000\",\n            #        \"data\": [\n            #            {\n            #                \"id\": \"615ba79f83a3410001cde321\",\n            #                \"symbol\": \"ETHUSDTM\",\n            #                \"autoDeposit\": False,\n            #                \"maintMarginReq\": 0.005,\n            #                \"riskLimit\": 1000000,\n            #                \"realLeverage\": 18.61,\n            #                \"crossMode\": False,\n            #                \"delevPercentage\": 0.86,\n            #                \"openingTimestamp\": 1638563515618,\n            #                \"currentTimestamp\": 1638576872774,\n            #                \"currentQty\": 2,\n            #                \"currentCost\": 83.64200000,\n            #                \"currentComm\": 0.05018520,\n            #                \"unrealisedCost\": 83.64200000,\n            #                \"realisedGrossCost\": 0.00000000,\n            #                \"realisedCost\": 0.05018520,\n            #                \"isOpen\": True,\n            #                \"markPrice\": 4225.01,\n            #                \"markValue\": 84.50020000,\n            #                \"posCost\": 83.64200000,\n            #                \"posCross\": 0.0000000000,\n            #                \"posInit\": 3.63660870,\n            #                \"posComm\": 0.05236717,\n            #                \"posLoss\": 0.00000000,\n            #                \"posMargin\": 3.68897586,\n            #                \"posMaint\": 0.50637594,\n            #                \"maintMargin\": 4.54717586,\n            #                \"realisedGrossPnl\": 0.00000000,\n            #                \"realisedPnl\": -0.05018520,\n            #                \"unrealisedPnl\": 0.85820000,\n            #                \"unrealisedPnlPcnt\": 0.0103,\n            #                \"unrealisedRoePcnt\": 0.2360,\n            #                \"avgEntryPrice\": 4182.10,\n            #                \"liquidationPrice\": 4023.00,\n            #                \"bankruptPrice\": 4000.25,\n            #                \"settleCurrency\": \"USDT\",\n            #                \"isInverse\": False\n            #            }\n            #        ]\n            #    }\n            #\n            data = client.safe_value(response, 'data')\n            return client.extend(client.parse_position(data, None), params)\n\n        return self.connector.adapter.adapt_position(\n            await fetch_position(self.connector.client, symbol, **kwargs)\n        )\n\n    async def set_symbol_partial_take_profit_stop_loss(self, symbol: str, inverse: bool,\n                                                       tp_sl_mode: trading_enums.TakeProfitStopLossMode):\n        \"\"\"\n        take profit / stop loss mode does not exist on kucoin\n        \"\"\"\n\n\nclass KucoinCCXTAdapter(exchanges.CCXTAdapter):\n    # Funding\n    KUCOIN_DEFAULT_FUNDING_TIME = 8 * commons_constants.HOURS_TO_SECONDS\n\n    # POSITION\n    KUCOIN_AUTO_DEPOSIT = \"autoDeposit\"\n\n    # ORDER\n    KUCOIN_LEVERAGE = \"leverage\"\n    KUCOIN_MARGIN_MODE = \"marginMode\"\n\n    def fix_order(self, raw, symbol=None, **kwargs):\n        fixed = super().fix_order(raw, symbol=symbol, **kwargs)\n        self._ensure_fees(fixed)\n        self._adapt_order_type(fixed)\n        return fixed\n\n    def fix_trades(self, raw, **kwargs):\n        fixed = super().fix_trades(raw, **kwargs)\n        for trade in fixed:\n            self._adapt_order_type(trade)\n            self._ensure_fees(trade)\n        return fixed\n\n    def _adapt_order_type(self, fixed):\n        order_info = fixed[trading_enums.ExchangeConstantsOrderColumns.INFO.value]\n        if fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] == \"liquid\":\n            # liquidation trades: considered as market orders\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = trading_enums.TradeOrderType.MARKET.value\n        if trigger_direction := order_info.get(\"stop\", None):\n            updated_type = trading_enums.TradeOrderType.UNKNOWN.value\n            \"\"\"\n            Stop Order Types (https://docs.kucoin.com/futures/#stop-orders)\n            down: Triggers when the price reaches or goes below the stopPrice.\n            up: Triggers when the price reaches or goes above the stopPrice.\n            \"\"\"\n            side = fixed.get(trading_enums.ExchangeConstantsOrderColumns.SIDE.value)\n            # SPOT: trigger_direction can be \"loss\" or  \"entry\"\n            # spot\n            is_stop_loss = False\n            is_stop_entry = False\n            trigger_above = False\n            # spot\n            if trigger_direction == \"loss\":\n                is_stop_loss = True\n            elif trigger_direction == \"entry\":\n                is_stop_entry = True\n            # futures\n            elif trigger_direction == \"up\":\n                trigger_above = True\n            elif trigger_direction == \"down\":\n                trigger_above = False\n            else:\n                # unhandled, rely on ccxt default parsing\n                self.logger.error(\n                    f\"Unhandled [{self.connector.exchange_manager.exchange_name}] {trigger_direction} order: skipped custom order type parsing ({fixed})\"\n                )\n                return fixed\n            if is_stop_loss:\n                trigger_above = side == trading_enums.TradeOrderSide.BUY.value\n            if is_stop_entry:\n                self.logger.error(\n                    f\"Unhandled [{self.connector.exchange_manager.exchange_name}] stop order type \"\n                    f\"{trigger_direction} ({fixed})\"\n                )\n            stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)\n            if side == trading_enums.TradeOrderSide.BUY.value:\n                if trigger_above:\n                    updated_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                else:\n                    # take profits are not yet handled as such: consider them as limit orders\n                    updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling\n                    if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:\n                        fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling\n            else:\n                # selling\n                if trigger_above:\n                    # take profits are not yet handled as such: consider them as limit orders\n                    updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling\n                    if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:\n                        fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling\n                else:\n                    updated_type = trading_enums.TradeOrderType.STOP_LOSS.value\n            # stop loss are not tagged as such by ccxt, force it\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above\n        return fixed\n\n    def parse_funding_rate(self, fixed, from_ticker=False, **kwargs):\n        \"\"\"\n        Kucoin next funding time is not provided\n        To obtain the last_funding_time :\n        => timestamp(previous_funding_timestamp) + timestamp(KUCOIN_DEFAULT_FUNDING_TIME)\n        \"\"\"\n        if from_ticker:\n            # no funding info in ticker\n            return {}\n        funding_dict = super().parse_funding_rate(fixed, from_ticker=from_ticker, **kwargs)\n        previous_funding_timestamp = fixed[trading_enums.ExchangeConstantsFundingColumns.NEXT_FUNDING_TIME.value]\n        fixed.update({\n            # patch LAST_FUNDING_TIME in tentacle\n            trading_enums.ExchangeConstantsFundingColumns.LAST_FUNDING_TIME.value:\n                previous_funding_timestamp,\n            # patch NEXT_FUNDING_TIME in tentacle\n            trading_enums.ExchangeConstantsFundingColumns.NEXT_FUNDING_TIME.value:\n                previous_funding_timestamp + self.KUCOIN_DEFAULT_FUNDING_TIME,\n        })\n        return funding_dict\n\n    def parse_position(self, fixed, **kwargs):\n        raw_position_info = fixed[ccxt_enums.ExchangePositionCCXTColumns.INFO.value]\n        parsed = super().parse_position(fixed, **kwargs)\n        parsed[trading_enums.ExchangeConstantsPositionColumns.AUTO_DEPOSIT_MARGIN.value] = (\n            raw_position_info.get(self.KUCOIN_AUTO_DEPOSIT, False)  # unset for cross positions\n        )\n        parsed_leverage = self.safe_decimal(\n            parsed, trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value, constants.ZERO\n        )\n        if parsed_leverage == constants.ZERO:\n            # on kucoin, fetched empty position don't have a leverage value. Since it's required within OctoBot,\n            # add it manually\n            symbol = parsed[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value]\n            if self.connector.exchange_manager.exchange.has_pair_future_contract(symbol):\n                parsed[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value] = \\\n                    self.connector.exchange_manager.exchange.get_pair_future_contract(symbol).current_leverage\n            else:\n                parsed[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value] = \\\n                    constants.DEFAULT_SYMBOL_LEVERAGE\n        return parsed\n"
  },
  {
    "path": "Trading/Exchange/kucoin/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Kucoin\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/kucoin_websocket_feed/__init__.py",
    "content": "from .kucoin_websocket import KucoinCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/kucoin_websocket_feed/kucoin_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.kucoin.kucoin_exchange as kucoin_exchange\n\n\nclass KucoinCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n    FUTURES_EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,  # not supported in futures\n        Feeds.TICKER: True,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,  # not supported in futures\n    }\n\n    SPOT_EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    IGNORED_FEED_PAIRS = {\n        # When ticker or future index is available : no need to calculate mark price from recent trades\n        # On kucoin, ticker feed is not containing close price: recent trades are required\n        # Feeds.TRADES: [Feeds.TICKER, Feeds.FUTURES_INDEX],\n        Feeds.TRADES: [Feeds.FUTURES_INDEX],\n        # When candles are available : use min timeframe kline to push ticker\n        Feeds.TICKER: [Feeds.KLINE]\n    }\n\n    # Feeds to create above which not to use websockets\n    # Kucoin raises \"exceed max permits per second\" when subscribing to more than 100 feeds\n    MAX_HANDLED_FEEDS = 100\n\n    RECREATE_CLIENT_ON_DISCONNECT = True   # when True, a new ccxt websocket client will replace the previous\n    # one when the exchange is disconnected\n\n    @classmethod\n    def get_name(cls):\n        return kucoin_exchange.Kucoin.get_name()\n\n    def get_feed_name(self):\n        if self.exchange_manager.is_future:\n            return kucoin_exchange.Kucoin.FUTURES_CCXT_CLASS_NAME\n        return super().get_feed_name()\n\n    @classmethod\n    def update_exchange_feeds(cls, exchange_manager):\n        if exchange_manager.is_future:\n            cls.EXCHANGE_FEEDS = cls.FUTURES_EXCHANGE_FEEDS\n        else:\n            cls.EXCHANGE_FEEDS = cls.SPOT_EXCHANGE_FEEDS\n\n    def get_adapter_class(self, adapter_class):\n        return kucoin_exchange.KucoinCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/kucoin_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"KucoinCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/kucoin_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/kucoin_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...kucoin_websocket_feed import KucoinCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    async with websocket_test_tools.ws_exchange_manager(config, KucoinCryptofeedWebsocketConnector.get_name()) \\\n            as exchange_manager_instance:\n        await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n            websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n            websocket_connector_class=KucoinCryptofeedWebsocketConnector,\n            exchange_manager=exchange_manager_instance,\n            config=config,\n            symbols=[\"BTC/USDT\", \"ETH/BTC\", \"ETH/USDT\"],\n            time_frames=[commons_enums.TimeFrames.ONE_HOUR],\n            expected_pushed_channels={\n                channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value\n            },\n            time_before_assert=20\n        )\n"
  },
  {
    "path": "Trading/Exchange/lbank/__init__.py",
    "content": "from .lbank_exchange import LBank"
  },
  {
    "path": "Trading/Exchange/lbank/lbank_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\nimport hashlib\nimport ccxt\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.constants as constants\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\n\n\nclass LBankSignConnectorMixin:\n    def __init__(self):\n        # used by default to force signed requests\n        self._force_signed_requests: typing.Optional[bool] = None\n\n    def _lazy_maybe_force_signed_requests(self, origin_ccxt_sign):\n        def lazy_sign(path, api, method, params, headers, body):\n            if self._force_signed_requests is None:\n                # force sign if the exchange requires authentication or if the connector is authenticated\n                self._force_signed_requests = self.exchange_manager.exchange.requires_authentication(\n                    self.exchange_manager.exchange.tentacle_config, None, None\n                ) or (\n                    self.exchange_manager.exchange.connector \n                    and self.exchange_manager.exchange.connector.is_authenticated\n                )\n                if self._force_signed_requests:\n                    self.logger.info(f\"Enabled force signing requests for {self.exchange_manager.exchange_name}\")\n            ccxt_sign_result = origin_ccxt_sign(path, api, method, params, headers, body)\n            if self._force_signed_requests:\n                if self.exchange_manager.exchange.is_authenticated_request(\n                    ccxt_sign_result.get(\"url\"), ccxt_sign_result.get(\"method\"), \n                    ccxt_sign_result.get(\"headers\"), ccxt_sign_result.get(\"body\")\n                ):\n                    # already signed\n                    return ccxt_sign_result\n                # force signature\n                return self._force_sign(path, api, method, params, headers, body)\n            return ccxt_sign_result\n        return lazy_sign\n\n    def _force_sign(self, path, api, method, params, headers, body):\n        self = self.client  # to use the same code as ccxt.async_support.lbank.sign (same self)\n        # same code as ccxt.async_support.lbank.sign but forced to sign\n        query = self.omit(params, self.extract_params(path))\n        url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params)\n        # Every spot endpoint ends with \".do\"\n        if api[0] == 'spot':\n            url += '.do'\n        else:\n            url = self.urls['api']['contract'] + '/' + self.implode_params(path, params)\n        # local override\n        # if api[1] == 'public':\n        #     if query:\n        #         url += '?' + self.urlencode(self.keysort(query))\n        # else:\n        # end local override\n        self.check_required_credentials()\n        timestamp = str(self.milliseconds())\n        echostr = self.uuid22() + self.uuid16()\n        query = self.extend({\n            'api_key': self.apiKey,\n        }, query)\n        signatureMethod = None\n        if len(self.secret) > 32:\n            signatureMethod = 'RSA'\n        else:\n            signatureMethod = 'HmacSHA256'\n        auth = self.rawencode(self.keysort(self.extend({\n            'echostr': echostr,\n            'signature_method': signatureMethod,\n            'timestamp': timestamp,\n        }, query)))\n        encoded = self.encode(auth)\n        hash = self.hash(encoded, 'md5')\n        uppercaseHash = hash.upper()\n        sign = None\n        if signatureMethod == 'RSA':\n            cacheSecretAsPem = self.safe_bool(self.options, 'cacheSecretAsPem', True)\n            pem = None\n            if cacheSecretAsPem:\n                pem = self.safe_value(self.options, 'pem')\n                if pem is None:\n                    pem = self.convert_secret_to_pem(self.encode(self.secret))\n                    self.options['pem'] = pem\n            else:\n                pem = self.convert_secret_to_pem(self.encode(self.secret))\n            sign = self.rsa(uppercaseHash, pem, 'sha256')\n        elif signatureMethod == 'HmacSHA256':\n            sign = self.hmac(self.encode(uppercaseHash), self.encode(self.secret), hashlib.sha256)\n        query['sign'] = sign\n        # local override\n        all_params = self.urlencode(self.keysort(query))\n        if api[1] == 'public':\n            if query:\n                url += '?' + all_params\n        else:\n            body = all_params\n        # end local override\n        headers = {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'timestamp': timestamp,\n            'signature_method': signatureMethod,\n            'echostr': echostr,\n        }\n        return {'url': url, 'method': method, 'body': body, 'headers': headers}\n\n\nclass LBankConnector(exchanges.CCXTConnector, LBankSignConnectorMixin):\n\n    def __init__(self, *args, **kwargs):\n        exchanges.CCXTConnector.__init__(self, *args, **kwargs)\n        LBankSignConnectorMixin.__init__(self)\n        # used by default to force signed requests\n        self._force_signed_requests: typing.Optional[bool] = None\n\n    def _create_client(self, force_unauth=False):\n        exchanges.CCXTConnector._create_client(self, force_unauth=force_unauth)\n        self.register_client_mocks()\n\n    def register_client_mocks(self):\n        self.client.sign = self._lazy_maybe_force_signed_requests(self.client.sign)\n        self.client.parse_order = self.parse_order_mock(self.client)\n    \n    def parse_order_mock(self, client):\n        origin_parse_order = client.parse_order\n        def _mocked_parse_order(order, market=None):\n            try:\n                return origin_parse_order(order, market)\n            except AttributeError as err:\n                if \"'NoneType' object has no attribute 'split'\" in str(err):\n                    # no order fetched\n                    raise ccxt.OrderNotFound(f\"Order not found\")\n                # should not happen\n                raise\n        return _mocked_parse_order\n\n\nclass LBank(exchanges.RestExchange):\n    DEFAULT_CONNECTOR_CLASS = LBankConnector\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    # should be fetched using recent trades.\n\n    @classmethod\n    def get_name(cls):\n        return 'lbank'\n\n    def get_adapter_class(self):\n        return LBankCCXTAdapter\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        # not supported\n        return constants.DEFAULT_ACCOUNT_ID\n\n    def get_additional_connector_config(self):\n        # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n        # (price should not be sent to market orders). Only used for buy market orders\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"createMarketBuyOrderRequiresPrice\": False  # disable quote conversion\n            }\n        }\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        body_signature_identifiers = \"sign=\"\n        header_signature_method_identifiers = \"signature_method\"\n        return bool(\n            headers\n            and header_signature_method_identifiers in headers\n        ) or bool(\n            body\n            and body_signature_identifiers in body\n        )\n\n\nclass LBankCCXTAdapter(exchanges.CCXTAdapter):\n    pass\n"
  },
  {
    "path": "Trading/Exchange/lbank/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"LBank\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/lbank/resources/lbank.md",
    "content": "LBank is a basic RestExchange adaptation for LBank exchange. \n"
  },
  {
    "path": "Trading/Exchange/lbank_websocket_feed/__init__.py",
    "content": "from .lbank_websocket import LBankCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/lbank_websocket_feed/lbank_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.time_frame_manager as time_frame_manager\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.lbank.lbank_exchange as lbank_exchange\n\n\nclass LBankCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector, lbank_exchange.LBankSignConnectorMixin):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n    FIX_CANDLES_TIMEZONE_IF_NEEDED: bool = True\n\n    def __init__(self, *args, **kwargs):\n        exchanges.CCXTWebsocketConnector.__init__(self, *args, **kwargs)\n        lbank_exchange.LBankSignConnectorMixin.__init__(self)\n\n    def _create_client(self):\n        exchanges.CCXTWebsocketConnector._create_client(self)\n        self.client.sign = self._lazy_maybe_force_signed_requests(self.client.sign)\n\n    def _should_authenticate(self):\n        return exchanges.CCXTWebsocketConnector._should_authenticate(self) or (\n            # oveerride to authenticate if the connector is authenticated\n            self.exchange_manager.exchange.connector \n            and self.exchange_manager.exchange.connector.is_authenticated\n        )\n\n    @classmethod\n    def get_name(cls):\n        return lbank_exchange.LBank.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return lbank_exchange.LBankCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/lbank_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"LBankCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/mexc/__init__.py",
    "content": "from .mexc_exchange import MEXC"
  },
  {
    "path": "Trading/Exchange/mexc/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"MEXC\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/mexc/mexc_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport contextlib\nimport decimal\nimport time\nimport typing\nimport ccxt\nimport hashlib\nfrom ccxt.base.types import Entry\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors\nimport octobot_commons.symbols as symbols_util\nimport octobot_commons.constants as commons_constants\nimport octobot_commons\nimport octobot_trading.constants as constants\n\n\nclass MEXCConnector(exchanges.CCXTConnector):\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._force_signed_requests: typing.Optional[bool] = None\n\n    def _create_client(self, force_unauth=False):\n        super()._create_client(force_unauth=force_unauth)\n        self.client.sign = self._lazy_maybe_force_signed_requests(self.client.sign)\n\n    def _lazy_maybe_force_signed_requests(self, origin_ccxt_sign):\n        def lazy_sign(path, api, method, params, headers, body):\n            if self._force_signed_requests is None:\n                self._force_signed_requests = self.exchange_manager.exchange.requires_authentication(\n                    self.exchange_manager.exchange.tentacle_config, None, None\n                )\n                if self._force_signed_requests:\n                    self.logger.info(f\"Enabled force signing requests for {self.exchange_manager.exchange_name}\")\n            ccxt_sign_result = origin_ccxt_sign(path, api, method, params, headers, body)\n            if self._force_signed_requests:\n                url = ccxt_sign_result.get(\"url\") or \"\"\n                ccxt_headers = ccxt_sign_result.get(\"headers\") or {}\n                if \"signature=\" in url or \"Signature\" in ccxt_headers:\n                    # already signed\n                    return ccxt_sign_result\n                # force signature\n                return self._force_sign(path, api, method, params, headers, body)\n            return ccxt_sign_result\n        return lazy_sign\n\n    def _force_sign(self, path, api, method, params, headers, body):\n        self = self.client  # to use the same code as ccxt.async_support.mexc.sign (same self)\n        # same code as ccxt.async_support.mexc.sign but forced to sign\n        section = self.safe_string(api, 0)\n        access = self.safe_string(api, 1)\n        path, params = self.resolve_path(path, params)\n        url = None\n        if section == 'spot' or section == 'broker':\n            if section == 'broker':\n                url = self.urls['api'][section][access] + '/' + path\n            else:\n                url = self.urls['api'][section][access] + '/api/' + self.version + '/' + path\n            urlParams = params\n            if True or access == 'private':  # local override to force signature\n                if section == 'broker' and ((method == 'POST') or (method == 'PUT') or (method == 'DELETE')):\n                    urlParams = {\n                        'timestamp': self.nonce(),\n                        'recvWindow': self.safe_integer(self.options, 'recvWindow', 5000),\n                    }\n                    body = self.json(params)\n                else:\n                    urlParams['timestamp'] = self.nonce()\n                    urlParams['recvWindow'] = self.safe_integer(self.options, 'recvWindow', 5000)\n            paramsEncoded = ''\n            if urlParams:\n                paramsEncoded = self.urlencode(urlParams)\n                url += '?' + paramsEncoded\n            if True or access == 'private':  # local override to force signature\n                self.check_required_credentials()\n                signature = self.hmac(self.encode(paramsEncoded), self.encode(self.secret), hashlib.sha256)\n                url += '&' + 'signature=' + signature\n                headers = {\n                    'X-MEXC-APIKEY': self.apiKey,\n                    'source': self.safe_string(self.options, 'broker', 'CCXT'),\n                }\n            if (method == 'POST') or (method == 'PUT') or (method == 'DELETE'):\n                headers['Content-Type'] = 'application/json'\n        elif section == 'contract' or section == 'spot2':\n            url = self.urls['api'][section][access] + '/' + self.implode_params(path, params)\n            params = self.omit(params, self.extract_params(path))\n            if False and access == 'public':  # local override to force signature\n                if params:\n                    url += '?' + self.urlencode(params)\n            else:\n                self.check_required_credentials()\n                timestamp = str(self.nonce())\n                auth = ''\n                headers = {\n                    'ApiKey': self.apiKey,\n                    'Request-Time': timestamp,\n                    'Content-Type': 'application/json',\n                    'source': self.safe_string(self.options, 'broker', 'CCXT'),\n                }\n                if method == 'POST':\n                    auth = self.json(params)\n                    body = auth\n                else:\n                    params = self.keysort(params)\n                    if params:\n                        auth += self.urlencode(params)\n                        url += '?' + auth\n                auth = self.apiKey + timestamp + auth\n                signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)\n                headers['Signature'] = signature\n        return {'url': url, 'method': method, 'body': body, 'headers': headers}\n\nclass MEXC(exchanges.RestExchange):\n    DEFAULT_CONNECTOR_CLASS = MEXCConnector\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n    # set True when disabled symbols should still be considered (ex: mexc with its temporary api trading disabled symbols)\n    # => avoid skipping untradable symbols\n    INCLUDE_DISABLED_SYMBOLS_IN_AVAILABLE_SYMBOLS = True\n    EXPECT_POSSIBLE_ORDER_NOT_FOUND_DURING_ORDER_CREATION = True  # set True when get_order() can return None\n    # (order not found) when orders are instantly filled on exchange and are not fully processed on the exchange side.\n\n    REQUIRE_ORDER_FEES_FROM_TRADES = True  # set True when get_order is not giving fees on closed orders and fees\n    # text content of errors due to unhandled authentication issues\n    # set True when create_market_buy_order_with_cost should be used to create buy market orders\n    # (useful to predict the exact spent amount)\n    ENABLE_SPOT_BUY_MARKET_WITH_COST = True\n\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'mexc {\"code\":700007,\"msg\":\"No permission to access the endpoint.\"}'\n        (\"no permission to access\",),\n    ]\n    EXCHANGE_AUTHENTICATION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'mexc {\"code\":10072,\"msg\":\"Api key info invalid\"}'\n        (\"api key info invalid\",),\n    ]\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # \"mexc {\"code\":700006,\"msg\":\"IP [33.33.33.33] not in the ip white list\"}\"\n        (\"not in the ip white list\",),\n    ]\n    # set when the exchange can allow users to pay fees in a custom currency (ex: BNB on binance)\n    LOCAL_FEES_CURRENCIES: typing.List[str] = [\"MX\"]\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    @classmethod\n    def get_name(cls):\n        return 'mexc'\n\n    def get_adapter_class(self):\n        return MEXCCCXTAdapter\n\n    def get_additional_connector_config(self):\n        # tell ccxt to use amount as provided and not to compute it by multiplying it by price which is done here\n        # (price should not be sent to market orders). Only used for buy market orders\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"createMarketBuyOrderRequiresPrice\": False,  # disable quote conversion\n                \"recvWindow\": 60000,  # default is 5000, avoid time related issues\n            }\n        }\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        # https://www.mexc.com/api-docs/spot-v3/spot-account-trade#query-uid\n        private_get_uid = Entry('uid', ['spot', 'private'], 'GET', {'cost': 10})\n        try:\n            resp = await private_get_uid.unbound_method(self.connector.client)\n            return str(resp[\"uid\"])\n        except Exception as err:\n            self.logger.exception(\n                err, \n                True, \n                f\"Unexpected error when getting {self.get_name()} account ID: {err}. Using default account ID.\"\n            )\n            return constants.DEFAULT_ACCOUNT_ID\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        # unknown (05/06/2025)\n        return super().get_max_orders_count(symbol, order_type)\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        url_signature_identifiers = \"signature=\"\n        header_signature_identifiers = \"Signature\"\n        return bool(\n            headers\n            and header_signature_identifiers in headers\n        ) or bool(\n            url\n            and url_signature_identifiers in url\n        )\n\n    async def get_all_tradable_symbols(self, active_only=True) -> set[str]:\n        \"\"\"\n        Override if the exchange is not allowing trading for all available symbols (ex: MEXC)\n        :return: the list of all symbols supported by the exchange that can currently be traded through API\n        \"\"\"\n        if CACHED_MEXC_API_HANDLED_SYMBOLS.should_be_updated():\n            await CACHED_MEXC_API_HANDLED_SYMBOLS.update(self)\n        return CACHED_MEXC_API_HANDLED_SYMBOLS.symbols\n\n    async def _create_specific_order(self, order_type, symbol, quantity: decimal.Decimal, price: decimal.Decimal = None,\n                                     side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None,\n                                     stop_price: decimal.Decimal = None, reduce_only: bool = False,\n                                     params=None) -> dict:\n        async with self._mexc_handled_symbols_filter(symbol):\n            return await super()._create_specific_order(order_type, symbol, quantity,\n                                                        price=price, stop_price=stop_price,\n                                                        side=side, current_price=current_price,\n                                                        reduce_only=reduce_only, params=params)\n\n    @contextlib.asynccontextmanager\n    async def _mexc_handled_symbols_filter(self, symbol):\n        try:\n            yield\n        except (ccxt.BadSymbol, ccxt.BadRequest) as err:\n            if \"symbol not support api\" in str(err):\n                raise octobot_trading.errors.UntradableSymbolError(\n                    f\"{self.get_name()} error: {symbol} trading pair is not available to the API at the moment, \"\n                    f\"{symbol} is under maintenance ({err}).\"\n                )\n            raise err\n\n    async def get_open_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:\n        return self._filter_orders(\n            await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs),\n            True\n        )\n\n    async def get_closed_orders(self, symbol: str = None, since: int = None, limit: int = None, **kwargs: dict) -> list:\n        return self._filter_orders(\n            await super().get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs),\n            False\n        )\n\n    async def get_order(\n        self,\n        exchange_order_id: str,\n        symbol: typing.Optional[str] = None,\n        order_type: typing.Optional[trading_enums.TraderOrderType] = None,\n        **kwargs: dict\n    ) -> dict:\n        try:\n            return await super().get_order(\n                exchange_order_id, symbol=symbol, order_type=order_type, **kwargs\n            )\n        except octobot_trading.errors.FailedRequest as err:\n            if \"Order does not exist\" in str(err):\n                return None\n            raise\n\n    def _filter_orders(self, orders: list, open_only: bool) -> list:\n        return [\n            order\n            for order in orders\n            if (\n                open_only and order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value]\n                == trading_enums.OrderStatus.OPEN.value\n            ) or (\n                not open_only and order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value]\n                != trading_enums.OrderStatus.OPEN.value\n            )\n        ]\n\n\nclass APIHandledSymbols:\n    \"\"\"\n    MEXC has pairs that are sometimes tradable from the exchange UI but not from the API. Get the list of\n    currently api tradable symbols from the defaultSymbols endpoint.\n    \"\"\"\n\n    def __init__(self, update_interval):\n        self.symbols = set()\n        self.last_update = 0\n        self._update_interval = update_interval\n\n    def should_be_updated(self):\n        return time.time() - self._update_interval >= self._update_interval\n\n    async def update(self, exchange):\n        try:\n            result = await exchange.connector.client.spot2_public_get_market_api_default_symbols()\n            self.symbols = set(\n                # in some cases, \"_\" is not replaced as symbol is not found in markets\n                exchange.connector.client.safe_market(s)[\"symbol\"].replace(\"_\", octobot_commons.MARKET_SEPARATOR)\n                for s in result[\"data\"][\"symbol\"]\n            )\n            self.last_update = time.time()\n            exchange.logger.info(f\"Updated handled symbols, list: {self.symbols}\")\n        except Exception as err:\n            exchange.logger.exception(err, True, f\"Error when fetching api-tradable symbols: {err}\")\n\n# make it available a singleton\nCACHED_MEXC_API_HANDLED_SYMBOLS = APIHandledSymbols(commons_constants.DAYS_TO_SECONDS)\n\nclass MEXCCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        try:\n            if fixed[\n                trading_enums.ExchangeConstantsOrderColumns.STATUS.value\n            ] == trading_enums.OrderStatus.CANCELED.value \\\n                    and fixed[trading_enums.ExchangeConstantsOrderColumns.FEE.value] is None:\n                symbol = fixed.get(trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value, \"\")\n                fixed[trading_enums.ExchangeConstantsOrderColumns.FEE.value] = {\n                    trading_enums.FeePropertyColumns.CURRENCY.value:\n                        symbols_util.parse_symbol(symbol).quote if symbol else \"\",\n                    trading_enums.FeePropertyColumns.COST.value: 0.0,\n                    trading_enums.FeePropertyColumns.IS_FROM_EXCHANGE.value: False,\n                    trading_enums.FeePropertyColumns.EXCHANGE_ORIGINAL_COST.value: 0.0,\n                }\n        except KeyError as err:\n            self.logger.debug(f\"Failed to fix order fees: {err}\")\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/mexc_websocket_feed/__init__.py",
    "content": "from .mexc_websocket import MEXCCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/mexc_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"MEXCCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/mexc_websocket_feed/mexc_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.mexc.mexc_exchange as mexc_exchange\n\n\nclass MEXCCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return mexc_exchange.MEXC.get_name()\n\n    def get_adapter_class(self, adapter_class):\n        return mexc_exchange.MEXCCCXTAdapter\n"
  },
  {
    "path": "Trading/Exchange/myokx/__init__.py",
    "content": "from .myokx_exchange import MyOkx"
  },
  {
    "path": "Trading/Exchange/myokx/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"MyOkx\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/myokx/myokx_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.enums as trading_enums\nimport tentacles.Trading.Exchange.okx.okx_exchange as okx_exchange\n\n\nclass MyOkx(okx_exchange.Okx):\n\n    @classmethod\n    def get_name(cls):\n        return 'myokx'\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n        ]\n"
  },
  {
    "path": "Trading/Exchange/myokx/resources/myokx.md",
    "content": "Okx is a basic RestExchange adaptation for MyOKX exchange. \n"
  },
  {
    "path": "Trading/Exchange/myokx_websocket_feed/__init__.py",
    "content": "from .myokx_websocket import MyOKXCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/myokx_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"MyOKXCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/myokx_websocket_feed/myokx_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.myokx.myokx_exchange as myokx_exchange\nimport tentacles.Trading.Exchange.okx_websocket_feed as okx_websocket_feed\n\n\nclass MyOKXCCXTWebsocketConnector(okx_websocket_feed.OKXCCXTWebsocketConnector):\n\n    @classmethod\n    def get_name(cls):\n        return myokx_exchange.MyOkx.get_name()\n"
  },
  {
    "path": "Trading/Exchange/ndax/__init__.py",
    "content": "from .ndax_exchange import Ndax"
  },
  {
    "path": "Trading/Exchange/ndax/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Ndax\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/ndax/ndax_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\n\n\nclass Ndax(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    DEFAULT_MAX_LIMIT = 500\n\n    @classmethod\n    def get_name(cls):\n        return 'ndax'\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict):\n        # ohlcv without limit is not supported, replaced by a default max limit\n        if limit is None:\n            limit = self.DEFAULT_MAX_LIMIT\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n\n"
  },
  {
    "path": "Trading/Exchange/ndax/resources/ndax.md",
    "content": "Ndax is a basic RestExchange adaptation for Ndax exchange. \n"
  },
  {
    "path": "Trading/Exchange/ndax/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/okcoin/__init__.py",
    "content": "from .okcoin_exchange import Okcoin"
  },
  {
    "path": "Trading/Exchange/okcoin/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Okcoin\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/okcoin/okcoin_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.okx as okx_tentacle\n\n\nclass Okcoin(okx_tentacle.Okx):\n    @classmethod\n    def get_name(cls):\n        return 'okcoin'\n"
  },
  {
    "path": "Trading/Exchange/okcoin/resources/okcoin.md",
    "content": "Okcoin is a basic RestExchange adaptation for Okcoin exchange. \n"
  },
  {
    "path": "Trading/Exchange/okcoin/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/okx/__init__.py",
    "content": "from .okx_exchange import Okx"
  },
  {
    "path": "Trading/Exchange/okx/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Okx\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/okx/okx_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.constants as constants\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\nimport octobot_trading.exchanges.connectors.ccxt.ccxt_connector as ccxt_connector\nimport octobot_trading.personal_data as trading_personal_data\n\n\n# def _disabled_okx_algo_order_creation(f):\n#     async def disabled_okx_algo_order_creation_wrapper(*args, **kwargs):\n#         # Algo order prevent bundled orders from working as they require to use the regular order api\n#         # Since the regular order api works for limit and market orders as well, us it all the time\n#         # Algo api is used for stop losses.\n#         # This ccxt issue will remain as long as privatePostTradeOrderAlgo will be used for each order with a\n#         # stopLossPrice or takeProfitPrice even when both are set (which make it an invalid okx algo order)\n#         connector = args[0]\n#         client = connector.client\n#         client.privatePostTradeOrderAlgo = client.privatePostTradeOrder\n#         try:\n#             return await f(*args, **kwargs)\n#         finally:\n#             client.privatePostTradeOrderAlgo = connector.get_saved_data(connector.PRIVATE_POST_TRADE_ORDER_ALGO)\n#     return disabled_okx_algo_order_creation_wrapper\n#\n#\n# def _enabled_okx_algo_order_creation(f):\n#     async def enabled_okx_algo_order_creation_wrapper(*args, **kwargs):\n#         # Used to force algo orders availability and avoid concurrency issues due to _disabled_algo_order_creation\n#         connector = args[0]\n#         connector.client.privatePostTradeOrderAlgo = connector.get_saved_data(connector.PRIVATE_POST_TRADE_ORDER_ALGO)\n#         return await f(*args, **kwargs)\n#     return enabled_okx_algo_order_creation_wrapper\n#\n#\n# class OkxConnector(ccxt_connector.CCXTConnector):\n#     PRIVATE_POST_TRADE_ORDER_ALGO = \"privatePostTradeOrderAlgo\"\n#\n#     def _create_client(self, force_unauth=False):\n#         super()._create_client(force_unauth=force_unauth)\n#         # save client.privatePostTradeOrderAlgo ref to prevent concurrent _disabled_algo_order_creation issues\n#         self.set_saved_data(self.PRIVATE_POST_TRADE_ORDER_ALGO, self.client.privatePostTradeOrderAlgo)\n#\n#     @_disabled_okx_algo_order_creation\n#     async def create_market_buy_order(self, symbol, quantity, price=None, params=None) -> dict:\n#         return await super().create_market_buy_order(symbol, quantity, price=price, params=params)\n#\n#     @_disabled_okx_algo_order_creation\n#     async def create_limit_buy_order(self, symbol, quantity, price=None, params=None) -> dict:\n#         return await super().create_limit_buy_order(symbol, quantity, price=price, params=params)\n#\n#     @_disabled_okx_algo_order_creation\n#     async def create_market_sell_order(self, symbol, quantity, price=None, params=None) -> dict:\n#         return await super().create_market_sell_order(symbol, quantity, price=price, params=params)\n#\n#     @_disabled_okx_algo_order_creation\n#     async def create_limit_sell_order(self, symbol, quantity, price=None, params=None) -> dict:\n#         return await super().create_limit_sell_order(symbol, quantity, price=price, params=params)\n#\n#     @_enabled_okx_algo_order_creation\n#     async def create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:\n#         return self.adapter.adapt_order(\n#             await self.client.create_order(\n#                 symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params\n#             ),\n#             symbol=symbol, quantity=quantity\n#         )\n\n\nclass Okx(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    # set True when even loading markets can make auth calls when creds are set\n    CAN_MAKE_AUTHENTICATED_REQUESTS_WHEN_LOADING_MARKETS = True\n\n    # text content of errors due to orders not found errors\n    EXCHANGE_PERMISSION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # OKX ex: okx {\"msg\":\"API key doesn't exist\",\"code\":\"50119\"}\n        (\"api\", \"key\", \"doesn't exist\"),\n    ]\n    # text content of errors due to account compliancy issues\n    EXCHANGE_COMPLIANCY_ERRORS: typing.List[typing.Iterable[str]] = [\n        # OKX ex: Trading of this pair or contract is restricted due to local compliance requirements\n        (\"restricted\", \"compliance\"),\n        # OKX ex: You can't trade this pair or borrow this crypto due to local compliance restrictions.\n        (\"restrictions\", \"compliance\"),\n    ]\n    # text content of errors due to unhandled authentication issues\n    EXCHANGE_AUTHENTICATION_ERRORS: typing.List[typing.Iterable[str]] = [\n        # 'okx {\"msg\":\"API key doesn't exist\",\"code\":\"50119\"}'\n        (\"api key doesn't exist\",),\n    ]\n    # text content of errors due to unhandled IP white list issues\n    EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [\n        # okx {\"msg\":\"Your IP 1.1.1.1 is not included in your API key's xxxx IP whitelist.\",\"code\":\"50110\"}\n        (\"is not included in your\", \"ip whitelist\"),\n    ]\n\n    FIX_MARKET_STATUS = True\n    ADAPT_MARKET_STATUS_FOR_CONTRACT_SIZE = True\n\n    # DEFAULT_CONNECTOR_CLASS = OkxConnector    # disabled until futures support is back\n    MAX_PAGINATION_LIMIT: int = 100  # value from https://www.okex.com/docs/en/#spot-orders_pending\n\n    # set when the exchange returns nothing when fetching historical candles with a too early start time\n    # (will iterate historical OHLCV requests over this window)\n    MAX_FETCHED_OHLCV_COUNT = 100\n\n    # Okx default take profits are market orders\n    # note: use BUY_MARKET and SELL_MARKET since in reality those are conditional market orders, which behave the same\n    # way as limit order but with higher fees\n    _OKX_BUNDLED_ORDERS = [trading_enums.TraderOrderType.STOP_LOSS, trading_enums.TraderOrderType.TAKE_PROFIT,\n                           trading_enums.TraderOrderType.BUY_MARKET, trading_enums.TraderOrderType.SELL_MARKET]\n\n    # should be overridden locally to match exchange support\n    SUPPORTED_ELEMENTS = {\n        trading_enums.ExchangeTypes.FUTURE.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                # trading_enums.TraderOrderType.STOP_LOSS,    # supported on futures\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {\n                trading_enums.TraderOrderType.BUY_MARKET: _OKX_BUNDLED_ORDERS,\n                trading_enums.TraderOrderType.SELL_MARKET: _OKX_BUNDLED_ORDERS,\n                trading_enums.TraderOrderType.BUY_LIMIT: _OKX_BUNDLED_ORDERS,\n                trading_enums.TraderOrderType.SELL_LIMIT: _OKX_BUNDLED_ORDERS,\n            },\n        },\n        trading_enums.ExchangeTypes.SPOT.value: {\n            # order that should be self-managed by OctoBot\n            trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [\n                trading_enums.TraderOrderType.STOP_LOSS,\n                trading_enums.TraderOrderType.STOP_LOSS_LIMIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT,\n                trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,\n                trading_enums.TraderOrderType.TRAILING_STOP,\n                trading_enums.TraderOrderType.TRAILING_STOP_LIMIT\n            ],\n            # order that can be bundled together to create them all in one request\n            trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},\n        }\n    }\n\n    # Set True when exchange is not returning empty position details when fetching a position with a specified symbol\n    # Exchange will then fallback to self.get_mocked_empty_position when having get_position returning None\n    REQUIRES_MOCKED_EMPTY_POSITION = True   # https://www.okx.com/learn/complete-guide-to-okex-api-v5-upgrade#h-rest-2\n\n    # set True when get_positions() is not returning empty positions and should use get_position() instead\n    REQUIRES_SYMBOL_FOR_EMPTY_POSITION = True\n    ADJUST_FOR_TIME_DIFFERENCE = True  # set True when the client needs to adjust its requests for time difference with the server\n\n    @classmethod\n    def get_name(cls):\n        return 'okx'\n\n    def get_adapter_class(self):\n        return OKXCCXTAdapter\n\n    @classmethod\n    def is_supporting_sandbox(cls) -> bool:\n        return False\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:\n        signature_identifier = \"OK-ACCESS-SIGN\"\n        return bool(\n            headers\n            and signature_identifier in headers\n        )\n\n    def _fix_limit(self, limit: int) -> int:\n        return min(self.MAX_PAGINATION_LIMIT, limit) if limit else limit\n\n    async def get_account_id(self, **kwargs: dict) -> str:\n        accounts = await self.connector.client.fetch_accounts()\n        try:\n            with self.connector.error_describer():\n                return accounts[0][\"id\"]\n        except IndexError as err:\n            # should never happen as at least one account should be available\n            raise\n\n    def get_max_orders_count(self, symbol: str, order_type: trading_enums.TraderOrderType) -> int:\n        # unknown (05/06/2025)\n        return super().get_max_orders_count(symbol, order_type)\n\n    async def get_sub_account_list(self):\n        sub_account_list = (await self.connector.client.privateGetUsersSubaccountList()).get(\"data\", [])\n        if not sub_account_list:\n            return []\n        return [\n            {\n                trading_enums.SubAccountColumns.ID.value: sub_account.get(\"subAcct\", \"\"),\n                trading_enums.SubAccountColumns.NAME.value: sub_account.get(\"label\", \"\")\n            }\n            for sub_account in sub_account_list\n            if sub_account.get(\"enable\", False)\n        ]\n\n    def get_order_additional_params(self, order) -> dict:\n        params = {}\n        if self.exchange_manager.is_future:\n            params[\"reduceOnly\"] = order.reduce_only\n            params[ccxt_enums.ExchangeOrderCCXTColumns.MARGIN_MODE.value] = self._get_ccxt_margin_type(order.symbol)\n        return params\n\n    def get_bundled_order_parameters(self, order, stop_loss_price=None, take_profit_price=None) -> dict:\n        \"\"\"\n        Returns the updated params when this exchange supports orders created upon other orders fill\n        (ex: a stop loss created at the same time as a buy order)\n        :param order: the initial order\n        :param stop_loss_price: the bundled order stopLoss price\n        :param take_profit_price: the bundled order takeProfit price\n        :return: A dict with the necessary parameters to create the bundled order on exchange alongside the\n        base order in one request\n        \"\"\"\n        params = {}\n        if not (\n            trading_personal_data.is_stop_order(order.order_type) or\n            trading_personal_data.is_take_profit_order(order.order_type)\n        ):\n            # force non algo order \"order type\"\n            if isinstance(order, trading_personal_data.MarketOrder):\n                params[\"ordType\"] = \"market\"\n            elif isinstance(order, trading_personal_data.LimitOrder):\n                params[\"px\"] = str(order.origin_price)\n                params[\"ordType\"] = \"limit\"\n        if stop_loss_price is not None:\n            params[self.connector.adapter.OKX_STOP_LOSS_PRICE] = float(stop_loss_price)\n            params[\"slOrdPx\"] = -1  # execute as market order\n        if take_profit_price is not None:\n            params[self.connector.adapter.OKX_TAKE_PROFIT_PRICE] = float(take_profit_price)\n            params[\"tpOrdPx\"] = -1  # execute as market order\n        return params\n\n    async def _get_all_typed_orders(self, method, symbol=None, since=None, limit=None, **kwargs) -> list:\n        # todo replace by settings fetch_stop_order_in_different_request method when OKX will be stable again\n        limit = self._fix_limit(limit)\n        is_stop_order = kwargs.get(\"stop\", False)\n        if is_stop_order and self.connector.adapter.OKX_ORDER_TYPE not in kwargs:\n            kwargs[self.connector.adapter.OKX_ORDER_TYPE] = self.connector.adapter.OKX_CONDITIONAL_ORDER_TYPE\n        regular_orders = await method(symbol=symbol, since=since, limit=limit, **kwargs)\n        if is_stop_order:\n            # only require stop orders\n            return regular_orders\n        # add order types of order (different param in api endpoint)\n        other_orders = []\n        if self.exchange_manager.is_future:\n            # stop orders are futures only for now\n            for order_type in self._get_used_order_types():\n                kwargs[\"ordType\"] = order_type\n                other_orders += await method(symbol=symbol, since=since, limit=limit, **kwargs)\n        return regular_orders + other_orders\n\n    async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await self._get_all_typed_orders(\n            super().get_open_orders, symbol=symbol, since=since, limit=limit, **kwargs\n        )\n\n    async def get_closed_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:\n        return await self._get_all_typed_orders(\n            super().get_closed_orders, symbol=symbol, since=since, limit=limit, **kwargs\n        )\n\n    async def get_order(\n        self,\n        exchange_order_id: str,\n        symbol: typing.Optional[str] = None,\n        order_type: typing.Optional[trading_enums.TraderOrderType] = None,\n        **kwargs: dict\n    ) -> dict:\n        try:\n            order = await super().get_order(\n                exchange_order_id, symbol=symbol, order_type=order_type, **kwargs\n            )\n            return order\n        except trading_errors.NotSupported:\n            if kwargs.get(\"stop\", False):\n                # from ccxt 2.8.4\n                # fetchOrder() does not support stop orders, use fetchOpenOrders() fetchCanceledOrders() or fetchClosedOrders\n                return await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol=symbol, **kwargs)\n            raise\n\n    def order_request_kwargs_factory(\n        self, \n        exchange_order_id: str, \n        order_type: typing.Optional[trading_enums.TraderOrderType] = None, \n        **kwargs\n    ) -> dict:\n        params = kwargs or {}\n        try:\n            if \"stop\" not in params:\n                order_type = (\n                    order_type or \n                    self.exchange_manager.exchange_personal_data.orders_manager.get_order(\n                        None, exchange_order_id=exchange_order_id\n                    ).order_type\n                )\n                params[\"stop\"] = (\n                    trading_personal_data.is_stop_order(order_type)\n                    or trading_personal_data.is_take_profit_order(order_type)\n                )\n        except KeyError as err:\n            self.logger.warning(\n                f\"Order {exchange_order_id} not found in order manager: considering it a regular (no stop/take profit) order {err}\"\n            )\n        return params\n\n    def _is_oco_order(self, params):\n        return all(\n            oco_order_param in (params or {})\n            for oco_order_param in (\n                self.connector.adapter.OKX_STOP_LOSS_PRICE,\n                self.connector.adapter.OKX_TAKE_PROFIT_PRICE\n            )\n        )\n\n    async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,\n                           price: decimal.Decimal = None, stop_price: decimal.Decimal = None,\n                           side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None,\n                           reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]:\n        if self._is_oco_order(params):\n            raise trading_errors.NotSupported(\n                f\"OCO bundled orders (orders including both a stop loss and take profit price) \"\n                f\"are not yet supported on {self.get_name()}\"\n            )\n        return await super().create_order(order_type, symbol, quantity,\n                                          price=price, stop_price=stop_price,\n                                          side=side, current_price=current_price,\n                                          reduce_only=reduce_only, params=params)\n\n    def _get_ccxt_margin_type(self, symbol, contract=None):\n        if not self.exchange_manager.exchange.has_pair_future_contract(symbol):\n            raise KeyError(f\"{symbol} contract unavailable\")\n        contract = contract or self.exchange_manager.exchange.get_pair_future_contract(symbol)\n        return ccxt_enums.ExchangeMarginTypes.ISOLATED.value if contract.is_isolated() \\\n            else ccxt_enums.ExchangeMarginTypes.CROSS.value\n\n    def _get_margin_query_params(self, symbol, **kwargs):\n        pos_side = self.connector.adapter.OKX_ONE_WAY_MODE\n        if not self.exchange_manager.exchange.has_pair_future_contract(symbol):\n            raise KeyError(f\"{symbol} contract unavailable\")\n        else:\n            contract = self.exchange_manager.exchange.get_pair_future_contract(symbol)\n            if not contract.is_one_way_position_mode():\n                self.logger.debug(f\"Switching {symbol} position mode to one way\")\n                contract.set_position_mode(is_one_way=True, is_hedge=False)\n                # todo: handle other position sides when cross is supported\n            kwargs = kwargs or {}\n            kwargs.update({\n                self.connector.adapter.OKX_LEVER: float(contract.current_leverage),\n                self.connector.adapter.OKX_MARGIN_MODE: self._get_ccxt_margin_type(symbol, contract=contract),\n                self.connector.adapter.OKX_POS_SIDE: pos_side,\n            })\n        return kwargs\n\n    async def get_symbol_leverage(self, symbol: str, **kwargs: dict):\n        \"\"\"\n        :param symbol: the symbol\n        :return: the current symbol leverage multiplier\n        \"\"\"\n        kwargs = kwargs or {}\n        if ccxt_enums.ExchangePositionCCXTColumns.MARGIN_MODE.value not in kwargs:\n            margin_type = ccxt_enums.ExchangeMarginTypes.ISOLATED.value\n            try:\n                margin_type = self._get_ccxt_margin_type(symbol)\n            except KeyError:\n                pass\n            kwargs[ccxt_enums.ExchangePositionCCXTColumns.MARGIN_MODE.value] = margin_type\n        return await self.connector.get_symbol_leverage(symbol=symbol, **kwargs)\n\n    async def set_symbol_leverage(self, symbol: str, leverage: float, **kwargs):\n        \"\"\"\n        Set the symbol leverage\n        :param symbol: the symbol\n        :param leverage: the leverage\n        :return: the update result\n        \"\"\"\n        kwargs = self._get_margin_query_params(symbol, **kwargs)\n        kwargs.pop(self.connector.adapter.OKX_LEVER, None)\n        return await self.connector.set_symbol_leverage(leverage=leverage, symbol=symbol, **kwargs)\n\n    async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: dict):\n        kwargs = self._get_margin_query_params(symbol, **kwargs)\n        kwargs.pop(self.connector.adapter.OKX_MARGIN_MODE)\n        await super().set_symbol_margin_type(symbol, isolated, **kwargs)\n\n    async def get_position(self, symbol: str, **kwargs: dict) -> dict:\n        \"\"\"\n        Get the current user symbol position\n        :param symbol: the position symbol\n        :return: the user symbol position\n        \"\"\"\n        position = await super().get_position(symbol=symbol, **kwargs)\n        if position[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] == constants.ZERO:\n            await self._update_position_with_leverage_data(symbol, position)\n\n        if position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value] != symbol:\n            # happened in previous ccxt version, todo remove if no seen again\n            raise ValueError(\n                f\"Invalid position symbol: \"\n                f\"{position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value]}, \"\n                f\"expected {symbol}\"\n            )\n        return position\n\n    async def _update_position_with_leverage_data(self, symbol, position):\n        leverage_data = await self.get_symbol_leverage(symbol)\n        adapter = self.connector.adapter\n        OKX_info = leverage_data[ccxt_constants.CCXT_INFO]\n        position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] = \\\n            adapter.parse_position_mode(OKX_info[0][adapter.OKX_POS_SIDE])\n        position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] = \\\n            adapter.parse_margin_type(leverage_data[ccxt_enums.ExchangeLeverageCCXTColumns.MARGIN_MODE.value])\n        position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value] = \\\n            leverage_data[trading_enums.ExchangeConstantsLeveragePropertyColumns.LEVERAGE.value]\n\n    async def set_symbol_partial_take_profit_stop_loss(self, symbol: str, inverse: bool,\n                                                       tp_sl_mode: trading_enums.TakeProfitStopLossMode):\n        \"\"\"\n        take profit / stop loss mode does not exist on okx futures\n        \"\"\"\n\n    def _get_used_order_types(self):\n        return [\n            # stop orders\n            self.connector.adapter.OKX_CONDITIONAL_ORDER_TYPE,\n            # created with bundled orders including stop loss & take profit: unsupported for now\n            # self.connector.adapter.OKX_OCO_ORDER_TYPE,\n        ]\n\n\nclass OKXCCXTAdapter(exchanges.CCXTAdapter):\n    # ORDERS\n    OKX_ORDER_TYPE = \"ordType\"\n    OKX_TRIGGER_ORDER_TYPE = \"trigger\"\n    OKX_OCO_ORDER_TYPE = \"oco\"\n    OKX_CONDITIONAL_ORDER_TYPE = \"conditional\"\n    OKX_BASIC_ORDER_TYPES = [\"market\", \"limit\"]\n    OKX_LAST_PRICE = \"last\"\n    OKX_STOP_LOSS_PRICE = \"stopLossPrice\"\n    OKX_TAKE_PROFIT_PRICE = \"takeProfitPrice\"\n    OKX_STOP_LOSS_TRIGGER_PRICE = \"slTriggerPx\"\n    OKX_TAKE_PROFIT_TRIGGER_PRICE = \"tpTriggerPx\"\n\n    # POSITIONS\n    OKX_MARGIN_MODE = \"mgnMode\"\n    OKX_POS_SIDE = \"posSide\"\n    OKX_ONE_WAY_MODE = \"net\"\n\n    # LEVERAGE\n    OKX_LEVER = \"lever\"\n    DATA = \"data\"\n\n    # Funding\n    OKX_DEFAULT_FUNDING_TIME = 8 * commons_constants.HOURS_TO_SECONDS\n\n    def fix_order(self, raw, symbol=None, **kwargs):\n        fixed = super().fix_order(raw, symbol=symbol, **kwargs)\n        self._adapt_order_type(fixed)\n        return fixed\n\n    def _adapt_order_type(self, fixed):\n        order_info = fixed[trading_enums.ExchangeConstantsOrderColumns.INFO.value]\n        if fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value, None) not in self.OKX_BASIC_ORDER_TYPES:\n            trigger_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TRIGGER_PRICE.value, None)\n            last_price = order_info.get(self.OKX_LAST_PRICE, None)\n            stop_loss_trigger_price = order_info.get(self.OKX_STOP_LOSS_TRIGGER_PRICE, None)\n            take_profit_trigger_price = order_info.get(self.OKX_TAKE_PROFIT_TRIGGER_PRICE, None)\n            updated_type = trading_enums.TradeOrderType.UNKNOWN.value\n            if stop_loss_trigger_price and take_profit_trigger_price:\n                # OCO order, unsupported yet\n                self.logger.debug(f\"Unsupported OKX OCO (stop loss & take profit in a single order): {fixed}\")\n                updated_type = trading_enums.TradeOrderType.UNSUPPORTED.value\n            elif stop_loss_trigger_price is not None:\n                updated_type = trading_enums.TradeOrderType.STOP_LOSS.value\n            elif take_profit_trigger_price is not None:\n                updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value\n            elif last_price is not None:\n                last_price = float(last_price)\n                side = fixed[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]\n                if side == trading_enums.TradeOrderSide.BUY.value:\n                    # trigger stop loss buy when price goes bellow stop_price, untriggered when last price is above\n                    if last_price > trigger_price:\n                        updated_type = trading_enums.TradeOrderType.STOP_LOSS.value\n                    else:\n                        updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value\n                else:\n                    # trigger take profit sell when price goes above stop_price, untriggered when last price is bellow\n                    if last_price < trigger_price:\n                        updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value\n                    else:\n                        updated_type = trading_enums.TradeOrderType.STOP_LOSS.value\n            else:\n                self.logger.error(\n                    f\"Unknown [{self.connector.exchange_manager.exchange_name}] order type, order: {fixed}\"\n                )\n            # stop loss and take profits are not tagged as such by ccxt, force it\n            fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type\n        return fixed\n\n    def parse_position(self, fixed, force_empty=False, **kwargs):\n        parsed = super().parse_position(fixed, force_empty=force_empty, **kwargs)\n        # use isolated by default. Set in set_leverage\n        parsed[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] = \\\n            trading_enums.MarginType(\n                fixed.get(ccxt_enums.ExchangePositionCCXTColumns.MARGIN_MODE.value)\n                or trading_enums.MarginType.ISOLATED.value\n            )\n        # use one way by default. Set in set_leverage\n        if parsed[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] == constants.ZERO:\n            parsed[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] = \\\n                trading_enums.PositionMode.ONE_WAY\n        return parsed\n\n    def parse_margin_type(self, margin_mode):\n        if margin_mode == ccxt_enums.ExchangeMarginTypes.ISOLATED.value:\n            return trading_enums.MarginType.ISOLATED\n        elif margin_mode == ccxt_enums.ExchangeMarginTypes.CROSS.value:\n            return trading_enums.MarginType.CROSS\n        raise ValueError(margin_mode)\n\n    def parse_position_mode(self, position_mode):\n        if position_mode == self.OKX_ONE_WAY_MODE:\n            return trading_enums.PositionMode.ONE_WAY\n        return trading_enums.PositionMode.HEDGE\n\n    def parse_leverage(self, fixed, **kwargs):\n        fixed = super().parse_leverage(fixed, **kwargs)\n        leverages = [\n            fixed[ccxt_enums.ExchangeLeverageCCXTColumns.LONG_LEVERAGE.value],\n            fixed[ccxt_enums.ExchangeLeverageCCXTColumns.SHORT_LEVERAGE.value],\n        ]\n        fixed[trading_enums.ExchangeConstantsLeveragePropertyColumns.LEVERAGE.value] = \\\n            decimal.Decimal(str(leverages[0] or leverages[1]))\n        return fixed\n\n    def parse_funding_rate(self, fixed, from_ticker=False, **kwargs):\n        if from_ticker:\n            # no funding info in ticker\n            return {}\n        fixed = super().parse_funding_rate(fixed, from_ticker=from_ticker, **kwargs)\n        next_funding_timestamp = fixed[trading_enums.ExchangeConstantsFundingColumns.NEXT_FUNDING_TIME.value]\n        fixed.update({\n            # patch LAST_FUNDING_TIME in tentacle\n            trading_enums.ExchangeConstantsFundingColumns.LAST_FUNDING_TIME.value:\n                max(next_funding_timestamp - self.OKX_DEFAULT_FUNDING_TIME, 0)\n        })\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/okx/resources/okx.md",
    "content": "Okx is a basic RestExchange adaptation for OKX exchange. \n"
  },
  {
    "path": "Trading/Exchange/okx/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/okx_websocket_feed/__init__.py",
    "content": "from .okx_websocket import OKXCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/okx_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"OKXCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/okx_websocket_feed/okx_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.okx.okx_exchange as okx_exchange\n\n\nclass OKXCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return okx_exchange.Okx.get_name()\n"
  },
  {
    "path": "Trading/Exchange/okx_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/okx_websocket_feed/tests/test_unauthenticated_mocked_feeds.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport pytest\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests as commons_tests\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.util.test_tools.websocket_test_tools as websocket_test_tools\nfrom ...okx_websocket_feed import OKXCryptofeedWebsocketConnector\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_start_spot_websocket():\n    config = commons_tests.load_test_config()\n    async with websocket_test_tools.ws_exchange_manager(config, OKXCryptofeedWebsocketConnector.get_name()) \\\n            as exchange_manager_instance:\n        await websocket_test_tools.test_unauthenticated_push_to_channel_coverage_websocket(\n            websocket_exchange_class=exchanges.CryptofeedWebSocketExchange,\n            websocket_connector_class=OKXCryptofeedWebsocketConnector,\n            exchange_manager=exchange_manager_instance,\n            config=config,\n            symbols=[\"BTC/USDT\", \"ETH/USDT\"],\n            time_frames=[commons_enums.TimeFrames.ONE_HOUR],\n            expected_pushed_channels={\n                channels_name.OctoBotTradingChannelsName.MARK_PRICE_CHANNEL.value,\n            },\n            time_before_assert=20\n        )\n"
  },
  {
    "path": "Trading/Exchange/okxus/__init__.py",
    "content": "from .okxus_exchange import OkxUs"
  },
  {
    "path": "Trading/Exchange/okxus/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"OkxUs\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/okxus/okxus_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.enums as trading_enums\nimport tentacles.Trading.Exchange.okx.okx_exchange as okx_exchange\n\n\nclass OkxUs(okx_exchange.Okx):\n\n    @classmethod\n    def get_name(cls):\n        return 'okxus'\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n        ]\n"
  },
  {
    "path": "Trading/Exchange/okxus/resources/okxus.md",
    "content": "OkxUs is a basic RestExchange adaptation for OKX US exchange. \n"
  },
  {
    "path": "Trading/Exchange/okxus_websocket_feed/__init__.py",
    "content": "from .okxus_websocket import OkxUsCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/okxus_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"OkxUsCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/okxus_websocket_feed/okxus_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport tentacles.Trading.Exchange.okxus.okxus_exchange as okxus_exchange\nimport tentacles.Trading.Exchange.okx_websocket_feed as okx_websocket_feed\n\n\nclass OkxUsCCXTWebsocketConnector(okx_websocket_feed.OKXCCXTWebsocketConnector):\n\n    @classmethod\n    def get_name(cls):\n        return okxus_exchange.OkxUs.get_name()\n"
  },
  {
    "path": "Trading/Exchange/phemex/__init__.py",
    "content": "from .phemex_exchange import Phemex"
  },
  {
    "path": "Trading/Exchange/phemex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Phemex\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/phemex/phemex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport decimal\nimport typing\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\n\n\nclass Phemex(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    ALLOWED_OHLCV_LIMITS = [5, 10, 50, 100, 500, 1000]\n    FIX_MARKET_STATUS = True\n\n    @classmethod\n    def get_name(cls):\n        return 'phemex'\n\n    def get_adapter_class(self):\n        return PhemexCCXTAdapter\n\n    def _get_adapted_limit(self, limit):\n        prev = self.ALLOWED_OHLCV_LIMITS[0]\n        for adapted in self.ALLOWED_OHLCV_LIMITS:\n            if adapted > limit:\n                return prev\n            prev = adapted\n        return prev\n\n    async def get_symbol_prices(self, symbol, time_frame, limit: int = 500, **kwargs: dict):\n        if limit not in self.ALLOWED_OHLCV_LIMITS:\n            limit = self._get_adapted_limit(limit)\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n\n    async def get_kline_price(self, symbol: str, time_frame: commons_enums.TimeFrames,\n                              **kwargs: dict) -> typing.Optional[list]:\n        return (await self.get_symbol_prices(symbol, time_frame, limit=5))[-1:]\n\n    async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,\n                           price: decimal.Decimal = None, stop_price: decimal.Decimal = None,\n                           side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None,\n                           reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]:\n        if order_type is trading_enums.TraderOrderType.BUY_MARKET \\\n                or order_type is trading_enums.TraderOrderType.SELL_MARKET:\n            # remove price argument on market orders or ccxt will try to convert cost into amount and\n            # make rounding differences\n            price = None\n        return await super().create_order(order_type, symbol, quantity,\n                                          price=price, stop_price=stop_price,\n                                          side=side, current_price=current_price,\n                                          reduce_only=reduce_only, params=params)\n\n    def _get_ohlcv_params(self, time_frame, limit, **kwargs):\n        if limit is None:\n            return {}\n        to_time = self.connector.client.milliseconds()\n        time_frame_msec = commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MSECONDS_TO_MINUTE\n        kwargs.update({\n            \"from\": to_time - (time_frame_msec * (limit + 1)),\n            \"limit\": limit,\n        })\n        return kwargs\n\n    async def cancel_order(\n            self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict\n    ) -> trading_enums.OrderStatus:\n        order_status = await super().cancel_order(exchange_order_id, symbol, order_type, **kwargs)\n        if order_status == trading_enums.OrderStatus.PENDING_CANCEL:\n            # cancelled orders can't be fetched, consider as cancelled\n            order_status = trading_enums.OrderStatus.CANCELED\n        return order_status\n\n    async def get_order(\n        self,\n        exchange_order_id: str,\n        symbol: typing.Optional[str] = None,\n        order_type: typing.Optional[trading_enums.TraderOrderType] = None,\n        **kwargs: dict\n    ) -> dict:\n        if order := await self.connector.get_order(\n            symbol=symbol, exchange_order_id=exchange_order_id, order_type=order_type, **kwargs\n        ):\n            return order\n        # try from closed orders (get_order is not returning filled or cancelled orders)\n        if order := await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol):\n            return order\n        # try from trades (get_order is not returning filled or cancelled orders)\n        return await self._get_order_from_trades(symbol, exchange_order_id, {})\n\n    async def _get_order_from_trades(self, symbol, exchange_order_id, order_to_update):\n        # usually the last trade is the right one\n        for _ in range(3):\n            if (order := await self.get_order_from_trades(symbol, exchange_order_id, order_to_update)) is None:\n                await asyncio.sleep(3)\n            else:\n                return order\n        return None\n\n\nclass PhemexCCXTAdapter(exchanges.CCXTAdapter):\n    PHEMEX_FEE_CURRENCY = \"feeCurrency\"\n\n    def fix_order(self, raw, **kwargs):\n        fixed = super().fix_order(raw, **kwargs)\n        try:\n            if fixed[\n                trading_enums.ExchangeConstantsOrderColumns.STATUS.value\n            ] == trading_enums.OrderStatus.CLOSED.value \\\n                    and fixed[trading_enums.ExchangeConstantsOrderColumns.FEE.value][\n               trading_enums.FeePropertyColumns.CURRENCY.value] is None:\n                order_info = fixed[trading_enums.ExchangeConstantsOrderColumns.INFO.value]\n                fixed[trading_enums.ExchangeConstantsOrderColumns.FEE.value][\n                    trading_enums.FeePropertyColumns.CURRENCY.value] = order_info[self.PHEMEX_FEE_CURRENCY]\n        except KeyError as err:\n            self.logger.debug(f\"Failed to fix order fees: {err}\")\n        try:\n            if fixed[\n                trading_enums.ExchangeConstantsOrderColumns.STATUS.value\n            ] == trading_enums.OrderStatus.CLOSED.value:\n                base_amount = fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value]\n                fixed[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] = \\\n                    base_amount - fixed[trading_enums.ExchangeConstantsOrderColumns.REMAINING.value]\n        except KeyError as err:\n            self.logger.debug(f\"Failed to fix order amount: {err}\")\n        return fixed\n"
  },
  {
    "path": "Trading/Exchange/phemex/resources/phemex.md",
    "content": "Phemex is a basic RestExchange adaptation for Phemex exchange. \n"
  },
  {
    "path": "Trading/Exchange/phemex/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/phemex_websocket_feed/__init__.py",
    "content": "from .phemex_websocket import PhemexCCXTWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/phemex_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"PhemexCCXTWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/phemex_websocket_feed/phemex_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.phemex.phemex_exchange as phemex_exchange\n\n\nclass PhemexCCXTWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: True,\n        Feeds.TICKER: True,\n        Feeds.CANDLE: True,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return phemex_exchange.Phemex.get_name()\n"
  },
  {
    "path": "Trading/Exchange/phemex_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/poloniex/__init__.py",
    "content": "from .poloniex_exchange import Poloniex"
  },
  {
    "path": "Trading/Exchange/poloniex/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Poloniex\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/poloniex/poloniex_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.exchanges as exchanges\n\n\nclass Poloniex(exchanges.RestExchange):\n    FIX_MARKET_STATUS = True\n    REMOVE_MARKET_STATUS_PRICE_LIMITS = True\n\n    @classmethod\n    def get_name(cls):\n        return 'poloniex'\n"
  },
  {
    "path": "Trading/Exchange/poloniex/resources/poloniex.md",
    "content": "Poloniex is a basic RestExchange adaptation for Poloniex exchange. \n"
  },
  {
    "path": "Trading/Exchange/poloniex/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/polymarket/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .ccxt import CCXTPolymarketExchange, CCXTAsyncPolymarketExchange, CCXTProPolymarketExchange\nfrom .polymarket_exchange import Polymarket\n"
  },
  {
    "path": "Trading/Exchange/polymarket/ccxt/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .polymarket_sync import polymarket as CCXTPolymarketExchange\nfrom .polymarket_async import polymarket as CCXTAsyncPolymarketExchange\nfrom .polymarket_pro import polymarket as CCXTProPolymarketExchange\n\nimport ccxt\nccxt.__all__.append(\"polymarket\")\nccxt.exchanges.append(\"polymarket\")\nccxt.polymarket = CCXTPolymarketExchange\n\nimport ccxt.async_support\nccxt.async_support.__all__.append(\"polymarket\")\nccxt.async_support.exchanges.append(\"polymarket\")\nccxt.async_support.polymarket = CCXTAsyncPolymarketExchange\n\nimport ccxt.pro\nccxt.pro.exchanges.append(\"polymarket\")\nccxt.pro.polymarket = CCXTProPolymarketExchange\n"
  },
  {
    "path": "Trading/Exchange/polymarket/ccxt/polymarket_abstract.py",
    "content": "from ccxt.base.types import Entry\n\n\nclass ImplicitAPI:\n    gamma_public_get_markets = gammaPublicGetMarkets = Entry('markets', ['gamma', 'public'], 'GET', {'cost': 1.6})\n    gamma_public_get_markets_id = gammaPublicGetMarketsId = Entry('markets/{id}', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_markets_id_tags = gammaPublicGetMarketsIdTags = Entry('markets/{id}/tags', ['gamma', 'public'], 'GET', {'cost': 2})\n    gamma_public_get_markets_slug_slug = gammaPublicGetMarketsSlugSlug = Entry('markets/slug/{slug}', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_events = gammaPublicGetEvents = Entry('events', ['gamma', 'public'], 'GET', {'cost': 2})\n    gamma_public_get_events_id = gammaPublicGetEventsId = Entry('events/{id}', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_series = gammaPublicGetSeries = Entry('series', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_series_id = gammaPublicGetSeriesId = Entry('series/{id}', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_search = gammaPublicGetSearch = Entry('search', ['gamma', 'public'], 'GET', {'cost': 0.667})\n    gamma_public_get_comments = gammaPublicGetComments = Entry('comments', ['gamma', 'public'], 'GET', {'cost': 2})\n    gamma_public_get_comments_id = gammaPublicGetCommentsId = Entry('comments/{id}', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_sports = gammaPublicGetSports = Entry('sports', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    gamma_public_get_sports_id = gammaPublicGetSportsId = Entry('sports/{id}', ['gamma', 'public'], 'GET', {'cost': 0.267})\n    data_public_get_positions = dataPublicGetPositions = Entry('positions', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_trades = dataPublicGetTrades = Entry('trades', ['data', 'public'], 'GET', {'cost': 2.67})\n    data_public_get_activity = dataPublicGetActivity = Entry('activity', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_holders = dataPublicGetHolders = Entry('holders', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_value = dataPublicGetValue = Entry('value', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_closed_positions = dataPublicGetClosedPositions = Entry('closed-positions', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_traded = dataPublicGetTraded = Entry('traded', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_oi = dataPublicGetOi = Entry('oi', ['data', 'public'], 'GET', {'cost': 1})\n    data_public_get_live_volume = dataPublicGetLiveVolume = Entry('live-volume', ['data', 'public'], 'GET', {'cost': 1})\n    bridge_public_get_supported_assets = bridgePublicGetSupportedAssets = Entry('supported-assets', ['bridge', 'public'], 'GET', {'cost': 1})\n    bridge_public_post_deposit = bridgePublicPostDeposit = Entry('deposit', ['bridge', 'public'], 'POST', {'cost': 1})\n    clob_public_get_orderbook = clobPublicGetOrderbook = Entry('orderbook', ['clob', 'public'], 'GET', {'cost': 1})\n    clob_public_get_orderbook_token_id = clobPublicGetOrderbookTokenId = Entry('orderbook/{token_id}', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_market_condition_id_trades = clobPublicGetMarketConditionIdTrades = Entry('market/{condition_id}/trades', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_trades = clobPublicGetTrades = Entry('trades', ['clob', 'public'], 'GET', {'cost': 0.667})\n    clob_public_get_prices_history = clobPublicGetPricesHistory = Entry('prices-history', ['clob', 'public'], 'GET', {'cost': 2})\n    clob_public_get_price = clobPublicGetPrice = Entry('price', ['clob', 'public'], 'GET', {'cost': 1})\n    clob_public_get_prices = clobPublicGetPrices = Entry('prices', ['clob', 'public'], 'GET', {'cost': 2.5})\n    clob_public_get_midpoint = clobPublicGetMidpoint = Entry('midpoint', ['clob', 'public'], 'GET', {'cost': 1})\n    clob_public_get_midpoints = clobPublicGetMidpoints = Entry('midpoints', ['clob', 'public'], 'GET', {'cost': 2.5})\n    clob_public_get_spread = clobPublicGetSpread = Entry('spread', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_last_trade_price = clobPublicGetLastTradePrice = Entry('last-trade-price', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_last_trades_prices = clobPublicGetLastTradesPrices = Entry('last-trades-prices', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get = clobPublicGet = Entry('', ['clob', 'public'], 'GET', {'cost': 4})\n    clob_public_get_time = clobPublicGetTime = Entry('time', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_tick_size = clobPublicGetTickSize = Entry('tick-size', ['clob', 'public'], 'GET', {'cost': 4})\n    clob_public_get_neg_risk = clobPublicGetNegRisk = Entry('neg-risk', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_fee_rate = clobPublicGetFeeRate = Entry('fee-rate', ['clob', 'public'], 'GET', {'cost': 0.04})\n    clob_public_get_markets = clobPublicGetMarkets = Entry('markets', ['clob', 'public'], 'GET', {'cost': 2})\n    clob_public_post_books = clobPublicPostBooks = Entry('books', ['clob', 'public'], 'POST', {'cost': 2.5})\n    clob_public_post_spreads = clobPublicPostSpreads = Entry('spreads', ['clob', 'public'], 'POST', {'cost': 0.04})\n    clob_public_post_prices = clobPublicPostPrices = Entry('prices', ['clob', 'public'], 'POST', {'cost': 2.5})\n    clob_private_get_order = clobPrivateGetOrder = Entry('order', ['clob', 'private'], 'GET', {'cost': 0.667})\n    clob_private_get_orders = clobPrivateGetOrders = Entry('orders', ['clob', 'private'], 'GET', {'cost': 1.33})\n    clob_private_get_trades = clobPrivateGetTrades = Entry('trades', ['clob', 'private'], 'GET', {'cost': 0.667})\n    clob_private_get_builder_trades = clobPrivateGetBuilderTrades = Entry('builder-trades', ['clob', 'private'], 'GET', {'cost': 0.667})\n    clob_private_get_notifications = clobPrivateGetNotifications = Entry('notifications', ['clob', 'private'], 'GET', {'cost': 1.6})\n    clob_private_get_balance_allowance = clobPrivateGetBalanceAllowance = Entry('balance-allowance', ['clob', 'private'], 'GET', {'cost': 1.6})\n    clob_private_get_order_scoring = clobPrivateGetOrderScoring = Entry('order-scoring', ['clob', 'private'], 'GET', {'cost': 0.04})\n    clob_private_get_auth_derive_api_key = clobPrivateGetAuthDeriveApiKey = Entry('auth/derive-api-key', ['clob', 'private'], 'GET', {'cost': 4})\n    clob_private_post_order = clobPrivatePostOrder = Entry('order', ['clob', 'private'], 'POST', {'cost': 0.5})\n    clob_private_post_orders = clobPrivatePostOrders = Entry('orders', ['clob', 'private'], 'POST', {'cost': 1})\n    clob_private_post_orders_scoring = clobPrivatePostOrdersScoring = Entry('orders-scoring', ['clob', 'private'], 'POST', {'cost': 0.04})\n    clob_private_post_auth_api_key = clobPrivatePostAuthApiKey = Entry('auth/api-key', ['clob', 'private'], 'POST', {'cost': 4})\n    clob_private_delete_order = clobPrivateDeleteOrder = Entry('order', ['clob', 'private'], 'DELETE', {'cost': 0.5})\n    clob_private_delete_orders = clobPrivateDeleteOrders = Entry('orders', ['clob', 'private'], 'DELETE', {'cost': 1})\n    clob_private_delete_cancel_all = clobPrivateDeleteCancelAll = Entry('cancel-all', ['clob', 'private'], 'DELETE', {'cost': 4})\n    clob_private_delete_cancel_market_orders = clobPrivateDeleteCancelMarketOrders = Entry('cancel-market-orders', ['clob', 'private'], 'DELETE', {'cost': 1})\n    clob_private_delete_notifications = clobPrivateDeleteNotifications = Entry('notifications', ['clob', 'private'], 'DELETE', {'cost': 0.04})\n    clob_private_put_balance_allowance = clobPrivatePutBalanceAllowance = Entry('balance-allowance', ['clob', 'private'], 'PUT', {'cost': 10})\n"
  },
  {
    "path": "Trading/Exchange/polymarket/ccxt/polymarket_async.py",
    "content": "# -*- coding: utf-8 -*-\n\n# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:\n# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code\n\nfrom ccxt.async_support.base.exchange import Exchange\nfrom .polymarket_abstract import ImplicitAPI\nimport hashlib\nimport math\nimport json\nimport numbers\nfrom ccxt.base.types import Any, Int, Market, MarketType, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFeeInterface\nfrom typing import List\nfrom ccxt.base.errors import ExchangeError\nfrom ccxt.base.errors import AuthenticationError\nfrom ccxt.base.errors import PermissionDenied\nfrom ccxt.base.errors import ArgumentsRequired\nfrom ccxt.base.errors import BadRequest\nfrom ccxt.base.errors import InsufficientFunds\nfrom ccxt.base.errors import InvalidOrder\nfrom ccxt.base.errors import OrderNotFound\nfrom ccxt.base.errors import NetworkError\nfrom ccxt.base.errors import RateLimitExceeded\nfrom ccxt.base.errors import ExchangeNotAvailable\nfrom ccxt.base.errors import OnMaintenance\nfrom ccxt.base.decimal_to_precision import ROUND\nfrom ccxt.base.decimal_to_precision import TICK_SIZE\nfrom ccxt.base.precise import Precise\n\n\nclass polymarket(Exchange, ImplicitAPI):\n\n    def describe(self) -> Any:\n        return self.deep_extend(super(polymarket, self).describe(), {\n            'id': 'polymarket',\n            'name': 'Polymarket',\n            'countries': ['US'],\n            'version': '1',\n            # Rate limits are enforced using Cloudflare's throttling system\n            # Requests over the limit are throttled/delayed rather than rejected\n            # See https://docs.polymarket.com/quickstart/introduction/rate-limits\n            # Cost calculation formula: cost = (1000 / rateLimit) * 60 / requests_per_minute\n            # With rateLimit = 50ms(20 req/s = 1200 req/min), base cost = 1.0\n            # General limits:\n            # - General Rate Limiting: 5000 requests / 10s(500 req/s = 30000 req/min) => cost = 0.04\n            # - CLOB(General): 5000 requests / 10s(500 req/s = 30000 req/min) => cost = 0.04\n            # - GAMMA(General): 750 requests / 10s(75 req/s = 4500 req/min) => cost = 0.267\n            # - Data API(General): 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n            # Setting to 50ms(20 req/s) to match the most restrictive general limit(Data API)\n            # Specific endpoint costs are calculated relative to self base rateLimit\n            'rateLimit': 50,  # 20 requests per second(matches Data API general limit)\n            'certified': False,\n            'pro': True,\n            'requiredCredentials': {\n                'apiKey': False,\n                'secret': False,\n                'walletAddress': True,\n                'privateKey': True,\n            },\n            'has': {\n                'CORS': None,\n                'spot': False,\n                'margin': False,\n                'swap': False,\n                'future': False,\n                'option': True,\n                'addMargin': False,\n                'cancelOrder': True,\n                'cancelOrders': True,\n                'createDepositAddress': True,  # TODO with https://docs.polymarket.com/developers/misc-endpoints/bridge-deposit\n                'createMarketBuyOrderWithCost': False,\n                'createMarketOrder': True,\n                'createMarketOrderWithCost': False,\n                'createMarketSellOrderWithCost': False,\n                'createOrder': True,\n                'createOrders': True,\n                'createStopLimitOrder': False,\n                'createStopMarketOrder': False,\n                'createStopOrder': False,\n                'editOrder': False,\n                'fetchBalance': True,\n                'fetchBorrowInterest': False,\n                'fetchBorrowRateHistories': False,\n                'fetchBorrowRateHistory': False,\n                'fetchClosedOrders': False,\n                'fetchCrossBorrowRate': False,\n                'fetchCrossBorrowRates': False,\n                'fetchCurrencies': False,\n                'fetchDepositAddress': False, \n                'fetchDepositAddresses': True,  # TODO with https://docs.polymarket.com/developers/misc-endpoints/bridge-supported-assets\n                'fetchDepositAddressesByNetwork': True,  # TODO with https://docs.polymarket.com/developers/misc-endpoints/bridge-supported-assets\n                'fetchDeposits': False,\n                'fetchFundingHistory': False,\n                'fetchFundingRate': False,\n                'fetchFundingRateHistory': False,\n                'fetchFundingRates': False,\n                'fetchIndexOHLCV': False,\n                'fetchIsolatedBorrowRate': False,\n                'fetchIsolatedBorrowRates': False,\n                'fetchLedger': False,\n                'fetchLedgerEntry': False,\n                'fetchLeverageTiers': False,\n                'fetchMarkets': True,\n                'fetchMarkOHLCV': False,\n                'fetchMyTrades': True,\n                'fetchOHLCV': True,\n                'fetchOpenInterest': True,\n                'fetchOpenInterestHistory': False,\n                'fetchOpenOrders': True,\n                'fetchOrder': True,\n                'fetchOrderBook': True,\n                'fetchOrderBooks': True,\n                'fetchOrders': True,\n                'fetchPositionMode': False,\n                'fetchPremiumIndexOHLCV': False,\n                'fetchStatus': True,\n                'fetchTicker': True,\n                'fetchTickers': True,\n                'fetchTime': True,\n                'fetchTrades': True,\n                'fetchTradingFee': True,\n                'fetchTradingFees': False,\n                'fetchWithdrawals': False,\n                'setLeverage': False,\n                'setMarginMode': False,\n                'transfer': False,\n                'withdraw': False,\n            },\n            'urls': {\n                'logo': 'https://polymarket.com/favicon.ico',\n                'api': {\n                    'gamma': 'https://gamma-api.polymarket.com',\n                    'clob': 'https://clob.polymarket.com',  # Can be overridden with options.clobHost\n                    'data': 'https://data-api.polymarket.com',\n                    'bridge': 'https://bridge.polymarket.com',\n                    'ws': 'wss://ws-subscriptions-clob.polymarket.com/ws/',  # CLOB WebSocket for subscriptions\n                    'rtds': 'wss://ws-live-data.polymarket.com',  # Real Time Data Socket for crypto prices and comments\n                },\n                'test': {},  # TODO if exists\n                'www': 'https://polymarket.com',\n                'doc': [\n                    'https://docs.polymarket.com',\n                ],\n                'fees': 'https://docs.polymarket.com/developers/CLOB/introduction',\n            },\n            'api': {\n                # GAMMA API: https://gamma-api.polymarket.com\n                # Rate limits: https://docs.polymarket.com/quickstart/introduction/rate-limits\n                # Cost calculation: cost = (1000 / 50) * 60 / requests_per_minute = 1200 / requests_per_minute\n                # - GAMMA(General): 750 requests / 10s(75 req/s = 4500 req/min) => cost = 0.267\n                # - GAMMA Get Comments: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA /events: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA /markets: 125 requests / 10s(12.5 req/s = 750 req/min) => cost = 1.6\n                # - GAMMA /markets /events listing: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA Tags: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA Search: 300 requests / 10s(30 req/s = 1800 req/min) => cost = 0.667\n                'gamma': {\n                    'public': {\n                        'get': {\n                            # Market endpoints\n                            'markets': 1.6,                     # GET /markets - used by fetchMarkets(125 req/10s = 750 req/min)\n                            'markets/{id}': 0.267,              # GET /markets/{id} - used by gammaPublicGetMarketsId(general limit)\n                            'markets/{id}/tags': 2.0,            # GET /markets/{id}/tags - used by gammaPublicGetMarketsIdTags(100 req/10s = 600 req/min)\n                            'markets/slug/{slug}': 0.267,        # GET /markets/slug/{slug} - used by gammaPublicGetMarketsSlugSlug(general limit)\n                            # Event endpoints\n                            'events': 2.0,                      # GET /events - used by gammaPublicGetEvents(100 req/10s = 600 req/min)\n                            'events/{id}': 0.267,                # GET /events/{id} - used by gammaPublicGetEventsId(general limit)\n                            # Series endpoints\n                            'series': 0.267,                     # GET /series - used by gammaPublicGetSeries(general limit)\n                            'series/{id}': 0.267,               # GET /series/{id} - used by gammaPublicGetSeriesId(general limit)\n                            # Search endpoints\n                            'search': 0.667,                     # GET /search - used by gammaPublicGetSearch(300 req/10s = 1800 req/min)\n                            # Comment endpoints\n                            'comments': 2.0,                     # GET /comments - used by gammaPublicGetComments(100 req/10s = 600 req/min)\n                            'comments/{id}': 0.267,             # GET /comments/{id} - used by gammaPublicGetCommentsId(general limit)\n                            # Sports endpoints\n                            'sports': 0.267,                    # GET /sports - used by gammaPublicGetSports(general limit)\n                            'sports/{id}': 0.267,               # GET /sports/{id} - used by gammaPublicGetSportsId(general limit)\n                        },\n                    },\n                },\n                # Data-API: https://data-api.polymarket.com\n                # Rate limits: https://docs.polymarket.com/quickstart/introduction/rate-limits\n                # Cost calculation: cost = (1000 / 50) * 60 / requests_per_minute = 1200 / requests_per_minute\n                # - Data API(General): 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - Data API(Alternative): 1200 requests / 1 minute(20 req/s = 1200 req/min) => cost = 1.0\n                # - Data API /trades: 75 requests / 10s(7.5 req/s = 450 req/min) => cost = 2.67\n                # - Data API \"OK\" Endpoint: 10 requests / 10s(1 req/s = 60 req/min) => cost = 20.0\n                'data': {\n                    'public': {\n                        'get': {\n                            # Core endpoints(from Data-API)\n                            'positions': 1.0,                     # GET /positions - used by dataPublicGetPositions(200 req/10s = 1200 req/min)\n                            'trades': 2.67,                      # GET /trades - used by dataPublicGetTrades(75 req/10s = 450 req/min)\n                            'activity': 1.0,                      # GET /activity - used by dataPublicGetActivity(200 req/10s = 1200 req/min)\n                            'holders': 1.0,                       # GET /holders - used by dataPublicGetHolders(200 req/10s = 1200 req/min)\n                            'value': 1.0,                         # GET /value - used by dataPublicGetTotalValue(200 req/10s = 1200 req/min)\n                            'closed-positions': 1.0,             # GET /closed-positions - used by dataPublicGetClosedPositions(200 req/10s = 1200 req/min)\n                            # Misc endpoints(from Data-API)\n                            'traded': 1.0,                        # GET /traded - used by dataPublicGetTraded(200 req/10s = 1200 req/min)\n                            'oi': 1.0,                            # GET /oi - used by dataPublicGetOpenInterest(200 req/10s = 1200 req/min)\n                            'live-volume': 1.0,                   # GET /live-volume - used by dataPublicGetLiveVolume(200 req/10s = 1200 req/min)\n                        },\n                    },\n                },\n                # Bridge API: https://bridge.polymarket.com\n                # Rate limits: Not explicitly documented, using conservative general rate limits\n                # Assuming similar to Data API: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                'bridge': {\n                    'public': {\n                        'get': {\n                            # Bridge endpoints\n                            'supported-assets': 1.0,              # GET /supported-assets - used by bridgePublicGetSupportedAssets(assumed 200 req/10s)\n                        },\n                        'post': {\n                            # Bridge endpoints\n                            'deposit': 1.0,                       # POST /deposit - used by bridgePublicPostDeposit(assumed 200 req/10s)\n                        },\n                    },\n                },\n                # CLOB API: https://clob.polymarket.com\n                # Rate limits: https://docs.polymarket.com/quickstart/introduction/rate-limits\n                # Cost calculation: cost = (1000 / 50) * 60 / requests_per_minute = 1200 / requests_per_minute\n                # General CLOB Endpoints:\n                # - CLOB(General): 5000 requests / 10s(500 req/s = 30000 req/min) => cost = 0.04\n                # - CLOB GET Balance Allowance: 125 requests / 10s(12.5 req/s = 750 req/min) => cost = 1.6\n                # - CLOB UPDATE Balance Allowance: 20 requests / 10s(2 req/s = 120 req/min) => cost = 10.0\n                # CLOB Market Data:\n                # - CLOB /book: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB /books: 80 requests / 10s(8 req/s = 480 req/min) => cost = 2.5\n                # - CLOB /price: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB /prices: 80 requests / 10s(8 req/s = 480 req/min) => cost = 2.5\n                # - CLOB /midprice: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB /midprices: 80 requests / 10s(8 req/s = 480 req/min) => cost = 2.5\n                # CLOB Ledger Endpoints:\n                # - CLOB Ledger(/trades /orders /notifications /order): 300 requests / 10s(30 req/s = 1800 req/min) => cost = 0.667\n                # - CLOB Ledger /data/orders: 150 requests / 10s(15 req/s = 900 req/min) => cost = 1.33\n                # - CLOB Ledger /data/trades: 150 requests / 10s(15 req/s = 900 req/min) => cost = 1.33\n                # - CLOB /notifications: 125 requests / 10s(12.5 req/s = 750 req/min) => cost = 1.6\n                # CLOB Markets & Pricing:\n                # - CLOB Price History: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - CLOB Markets: 250 requests / 10s(25 req/s = 1500 req/min) => cost = 0.8\n                # - CLOB Market Tick Size: 50 requests / 10s(5 req/s = 300 req/min) => cost = 4.0\n                # - CLOB markets/0x: 50 requests / 10s(5 req/s = 300 req/min) => cost = 4.0\n                # - CLOB /markets listing: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # CLOB Authentication:\n                # - CLOB API Keys: 50 requests / 10s(5 req/s = 300 req/min) => cost = 4.0\n                # CLOB Trading Endpoints(using sustained limits, not BURST):\n                # - CLOB POST /order: 24000 requests / 10 minutes(40 req/s = 2400 req/min) => cost = 0.5\n                # - CLOB DELETE /order: 24000 requests / 10 minutes(40 req/s = 2400 req/min) => cost = 0.5\n                # - CLOB POST /orders: 12000 requests / 10 minutes(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB DELETE /orders: 12000 requests / 10 minutes(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB DELETE /cancel-all: 3000 requests / 10 minutes(5 req/s = 300 req/min) => cost = 4.0\n                # - CLOB DELETE /cancel-market-orders: 12000 requests / 10 minutes(20 req/s = 1200 req/min) => cost = 1.0\n                'clob': {\n                    'public': {\n                        'get': {\n                            # Order book endpoints\n                            'orderbook': 1.0,                     # GET /book - used by fetchOrderBook(200 req/10s = 1200 req/min)\n                            'orderbook/{token_id}': 0.04,        # Not used(deprecated format, general limit)\n                            # Trade endpoints\n                            'market/{condition_id}/trades': 0.04,  # Not used(deprecated, use /trades instead, general limit)\n                            'trades': 0.667,                    # GET /data/trades - used by fetchTrades(300 req/10s = 1800 req/min)\n                            # Price history endpoints\n                            'prices-history': 2.0,              # GET /prices-history - used by fetchOHLCV(100 req/10s = 600 req/min)\n                            # Pricing endpoints\n                            'price': 1.0,                       # GET /price - available but using POST /prices instead(200 req/10s = 1200 req/min)\n                            'prices': 2.5,                      # GET /prices - used by fetchTickers(80 req/10s = 480 req/min)\n                            # Midpoint endpoints\n                            'midpoint': 1.0,                    # GET /midpoint - used by fetchTicker(200 req/10s = 1200 req/min)\n                            'midpoints': 2.5,                   # GET /midpoints - available for fetchTickers enhancement(80 req/10s = 480 req/min)\n                            # Spread endpoints\n                            'spread': 0.04,                     # GET /spread - available for fetchTicker enhancement(general limit)\n                            # Last trade price endpoints\n                            'last-trade-price': 0.04,           # GET /last-trade-price - available for ticker enhancement(general limit)\n                            'last-trades-prices': 0.04,         # GET /last-trades-prices - available for tickers enhancement(general limit)\n                            # Utility endpoints\n                            '': 4.0,                            # GET / - health check endpoint used by fetchStatus/clobPublicGetOk(50 req/10s = 300 req/min)\n                            'time': 0.04,                       # GET /time - used by fetchTime(general limit)\n                            'tick-size': 4.0,                   # GET /tick-size - used for market precision(50 req/10s = 300 req/min)\n                            'neg-risk': 0.04,                   # GET /neg-risk - used for market metadata(general limit)\n                            'fee-rate': 0.04,                   # GET /fee-rate - used by fetchTradingFee(general limit)\n                            'markets': 2.0,                     # GET /markets - used by fetchMarkets(100 req/10s = 600 req/min)\n                        },\n                        'post': {\n                            # Order book endpoints\n                            'books': 2.5,                      # POST /books - used by fetchOrderBooks(80 req/10s = 480 req/min)\n                            # Spread endpoints\n                            'spreads': 0.04,                    # POST /spreads - used by fetchTickers(optional, general limit)\n                            # Pricing endpoints\n                            'prices': 2.5,                      # POST /prices - used by fetchTicker(80 req/10s = 480 req/min)\n                        },\n                    },\n                    'private': {\n                        'get': {\n                            # Order endpoints\n                            'order': 0.667,                     # GET /data/order/{order_id} - used by fetchOrder(300 req/10s = 1800 req/min)\n                            'orders': 1.33,                     # GET /data/orders - used by fetchOrders, fetchOpenOrders(150 req/10s = 900 req/min)\n                            # Trade endpoints\n                            'trades': 0.667,                    # GET /data/trades - used by fetchMyTrades(300 req/10s = 1800 req/min)\n                            'builder-trades': 0.667,             # GET /builder-trades - used for builder trades(300 req/10s = 1800 req/min)\n                            # Notification endpoints\n                            'notifications': 1.6,                # GET /notifications - used by getNotifications(125 req/10s = 750 req/min)\n                            # Balance endpoints\n                            'balance-allowance': 1.6,           # GET /balance-allowance - used by fetchBalance/getBalanceAllowance(125 req/10s = 750 req/min)\n                            # Order scoring endpoints\n                            'order-scoring': 0.04,               # GET /order-scoring - used by isOrderScoring(general limit)\n                            # API credential endpoints(L1 authentication - uses manual URL building)\n                            'auth/derive-api-key': 4.0,         # GET /auth/derive-api-key - used by derive_api_key(50 req/10s = 300 req/min)\n                        },\n                        'post': {\n                            # Order creation endpoints\n                            'order': 0.5,                       # POST /order - used by createOrder(24000 req/10min = 2400 req/min sustained)\n                            'orders': 1.0,                      # POST /orders - used by createOrders(12000 req/10min = 1200 req/min sustained)\n                            # Order scoring endpoints\n                            'orders-scoring': 0.04,             # POST /orders-scoring - used by areOrdersScoring(general limit)\n                            # API credential endpoints\n                            'auth/api-key': 4.0,                # POST /auth/api-key - used by create_or_derive_api_creds(50 req/10s = 300 req/min)\n                        },\n                        'delete': {\n                            # Order cancellation endpoints\n                            'order': 0.5,                       # DELETE /order - used by cancelOrder(24000 req/10min = 2400 req/min sustained)\n                            'orders': 1.0,                      # DELETE /orders - used by cancelOrders(12000 req/10min = 1200 req/min sustained)\n                            'cancel-all': 4.0,                   # DELETE /cancel-all - used by cancelAllOrders(3000 req/10min = 300 req/min sustained)\n                            'cancel-market-orders': 1.0,        # DELETE /cancel-market-orders - used for canceling market orders(12000 req/10min = 1200 req/min sustained)\n                            # Notification endpoints\n                            'notifications': 0.04,               # DELETE /notifications - used by dropNotifications(general limit)\n                        },\n                        'put': {\n                            # Balance endpoints\n                            'balance-allowance': 10.0,           # PUT /balance-allowance - used by updateBalanceAllowance(20 req/10s = 120 req/min)\n                        },\n                    },\n                },\n            },\n            'timeframes': {\n                '1m': '1m',\n                '1h': '1h',\n                '6h': '6h',\n                '1d': '1d',\n                '1w': '1w',\n            },\n            'fees': {\n                'trading': {\n                    'tierBased': False,\n                    'percentage': True,\n                    'taker': self.parse_number('0.02'),  # 2% taker fee(approximate)\n                    'maker': self.parse_number('0.02'),  # 2% maker fee(approximate)\n                },\n            },\n            'options': {\n                'fetchMarkets': {\n                    'active': True,  # only fetch active markets by default\n                    'closed': False,\n                    'archived': False,\n                },\n                'funder': None,  # Address that holds funds(walletAddress, required for proxy wallets like email/Magic wallets)\n                'proxyWallet': None,  # Proxy wallet address for Data-API endpoints(defaults to funder/walletAddress if not set)\n                'builderWallet': None,  # Builder wallet address(defaults to funder/walletAddress if not set)\n                'signatureTypes': {\n                    # https://docs.polymarket.com/developers/CLOB/orders/orders#signature-types\n                    'EOA': 0,  # EIP712 signature signed by an EOA\n                    'POLY_PROXY': 1,  # EIP712 signatures signed by a signer associated with funding Polymarket proxy wallet\n                    'POLY_GNOSIS_SAFE': 2,  # EIP712 signatures signed by a signer associated with funding Polymarket gnosis safe wallet\n                },\n                'side': None,  # Order side: 'BUY' or 'SELL'(default: None, must be provided)\n                'sides': {\n                    'BUY': 0,  # Buy side(maker gives USDC, wants tokens)\n                    'SELL': 1,  # Sell side(maker gives tokens, wants USDC)\n                },\n                'chainId': 137,  # Chain ID: 137 = Polygon mainnet(default), 80001 = Polygon Mumbai testnet\n                'chainName': 'polygon-mainnet',  # Chain name: 'polygon-mainnet'(default), 'polygon-mumbai'(testnet)\n                'sandboxMode': False,  # Enable sandbox/testnet mode(uses Polygon Mumbai testnet)\n                'clobHost': None,  # Custom CLOB API endpoint(defaults to https://clob.polymarket.com)\n                'defaultCollateral': 'USDC',  # Default collateral currency\n                'defaultExpirationDays': 30,  # Default expiration in days(default: 30 days from now)\n                'defaultFeeRateBps': 200,  # Default fee rate fallback in basis points(default: 200 bps = 2%)\n                'defaultTickSize': '0.01',  # Default tick size for rounding config(default: 0.01 = 2 decimal places for price, 2 for size, 4 for amount)\n                'marketOrderQuoteDecimals': 2,  # Max decimal places for quote currency(USDC) in market orders(default: 2)\n                'marketOrderBaseDecimals': 4,  # Max decimal places for base currency(tokens) in market orders(default: 4)\n                'roundingBufferDecimals': 4,  # Additional decimal places buffer for rounding up before final rounding down(default: 4)\n                # Constants matching clob-client\n                # See https://github.com/Polymarket/clob-client/blob/main/src/signing/constants.ts\n                # See https://github.com/Polymarket/clob-client/blob/main/src/constants.ts\n                'clobDomainName': 'ClobAuthDomain',\n                'clobVersion': '1',\n                'msgToSign': 'This message attests that I control the given wallet',\n                'initialCursor': 'MA==',  # Base64 encoded empty string, matches clob-client INITIAL_CURSOR\n                'endCursor': 'LTE=',  # Sentinel value indicating end of pagination\n                'defaultTokenId': None,  # Default token ID for conditional tokens\n                # Constants matching py-clob-client\n                # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/constants.py\n                'zeroAddress': '0x0000000000000000000000000000000000000000',  # Zero address for open orders(taker)\n                # EIP-712 domain constants matching clob-order-utils\n                # See https://github.com/Polymarket/clob-order-utils/blob/main/src/exchange.order.const.ts\n                'orderDomainName': 'Polymarket CTF Exchange',  # EIP-712 domain name for orders(PROTOCOL_NAME)\n                'orderDomainVersion': '1',  # EIP-712 domain version for orders(PROTOCOL_VERSION)\n                # Contract addresses for all networks\n                # See https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n                'contracts': {\n                    # Polygon Amoy testnet(chainId: 80001)\n                    '80001': {\n                        'exchange': '0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40',\n                        'negRiskAdapter': '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296',\n                        'negRiskExchange': '0xC5d563A36AE78145C45a50134d48A1215220f80a',\n                        'collateral': '0x9c4e1703476e875070ee25b56a58b008cfb8fa78',\n                        'conditionalTokens': '0x69308FB512518e39F9b16112fA8d994F4e2Bf8bB',\n                    },\n                    # Polygon mainnet(chainId: 137)\n                    '137': {\n                        'exchange': '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E',\n                        'negRiskAdapter': '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296',\n                        'negRiskExchange': '0xC5d563A36AE78145C45a50134d48A1215220f80a',\n                        'collateral': '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',\n                        'conditionalTokens': '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045',\n                    },\n                },\n            },\n            'exceptions': {\n                'exact': {\n                    # HTTP status codes\n                    '400': BadRequest,  # Bad Request - Invalid request parameters\n                    '401': AuthenticationError,  # Unauthorized - Invalid or missing authentication\n                    '403': PermissionDenied,  # Forbidden - Insufficient permissions\n                    '404': ExchangeError,  # Not Found - Resource not found\n                    '429': RateLimitExceeded,  # Too Many Requests - Rate limit exceeded\n                    '500': ExchangeError,  # Internal Server Error\n                    '502': ExchangeError,  # Bad Gateway\n                    '503': OnMaintenance,  # Service Unavailable - Service temporarily unavailable\n                    '504': NetworkError,  # Gateway Timeout\n                    # Common error messages(will be matched against error/message fields in response)\n                    'Invalid signature': AuthenticationError,  # Invalid signature in request\n                    'Invalid API key': AuthenticationError,  # Invalid or missing API key\n                    'Invalid timestamp': AuthenticationError,  # Invalid timestamp in request\n                    'Signature expired': AuthenticationError,  # Request timestamp is too old\n                    'Unauthorized': AuthenticationError,  # Authentication failed\n                    'Forbidden': PermissionDenied,  # Access denied\n                    'Rate limit exceeded': RateLimitExceeded,  # Rate limit exceeded\n                    'Too many requests': RateLimitExceeded,  # Too many requests\n                    'Invalid order': InvalidOrder,  # Order validation failed\n                    'Invalid orderID': OrderNotFound,  # Order does not exist\n                    'Order not found': OrderNotFound,  # Order does not exist\n                    'Insufficient funds': InsufficientFunds,  # Insufficient balance\n                    'Insufficient balance': InsufficientFunds,  # Insufficient balance\n                    'Invalid market': BadRequest,  # Invalid market/symbol\n                    'Invalid symbol': BadRequest,  # Invalid symbol\n                    'Market not found': BadRequest,  # Market does not exist\n                    'Service unavailable': ExchangeNotAvailable,  # Service temporarily unavailable\n                    'Maintenance': OnMaintenance,  # Service under maintenance\n                },\n                'broad': {\n                    'authentication': AuthenticationError,  # Any authentication-related error\n                    'authorization': PermissionDenied,  # Any authorization-related error\n                    'rate limit': RateLimitExceeded,  # Any rate limit error\n                    'invalid order': InvalidOrder,  # Any order validation error\n                    'insufficient': InsufficientFunds,  # Any insufficient funds/balance error\n                    'not found': ExchangeError,  # Any not found error\n                    'timeout': NetworkError,  # Any timeout error\n                    'network': NetworkError,  # Any network-related error\n                    'maintenance': OnMaintenance,  # Any maintenance-related error\n                },\n            },\n        })\n\n    def get_signature_type(self, params={}):\n        \"\"\"\n Helper method to get signature type from params or options with fallback to constants\n        :param dict [params]: parameters that may contain signatureType or signature_type\n        :returns number|None: signature type value\n        \"\"\"\n        signatureTypes = self.safe_dict(self.options, 'signatureTypes', {})\n        eoaSignatureType = self.safe_integer(signatureTypes, 'EOA')\n        polyProxySignatureType = self.safe_integer(signatureTypes, 'POLY_PROXY')\n        polyGnosisSafeSignatureType = self.safe_integer(signatureTypes, 'POLY_GNOSIS_SAFE')\n        # Note: POLY_GNOSIS_SAFE is not supported for now\n        proxyWalletAddress = self.get_proxy_wallet_address()\n        mainWalletAddress = self.get_main_wallet_address()\n        if proxyWalletAddress != mainWalletAddress:\n            return polyProxySignatureType\n        return eoaSignatureType\n\n    def get_side(self, sideString: str, params={}):\n        \"\"\"\n Helper method to get side from params or options with fallback to constants\n Converts BUY/SELL string to integer: BUY = 0, SELL = 1(matches UtilsBuy/UtilsSell from py-order-utils)\n        :param str sideString: side('BUY' or 'SELL')\n        :param dict [params]: parameters that may contain side or side_int\n        :returns number: side(0 for BUY, 1 for SELL)\n        \"\"\"\n        # Check if side_int is provided directly in params\n        sideInt = self.safe_integer(params, 'sideInt') or self.safe_integer(params, 'side_int')\n        if sideInt is not None:\n            return sideInt\n        # Get sides enum from options\n        sides = self.safe_dict(self.options, 'sides', {})\n        buySide = self.safe_integer(sides, 'BUY', 0)\n        sellSide = self.safe_integer(sides, 'SELL', 1)\n        # Convert side string to integer\n        sideUpper = sideString.upper()\n        sideValue = sellSide  # Default to SELL\n        if sideUpper == 'BUY':\n            sideValue = buySide\n        return sideValue\n\n    async def fetch_markets(self, params={}) -> List[Market]:\n        \"\"\"\n        retrieves data on all markets for polymarket\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide#3-fetch-all-active-markets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param boolean [params.active]: fetch active markets only(default: True)\n        :param boolean [params.closed]: fetch closed markets\n        :returns dict[]: an array of objects representing market data\n        \"\"\"\n        limit = 500\n        options = self.safe_dict(self.options, 'fetchMarkets', {})\n        request: dict = self.extend({\n            'order': 'id',\n            'ascending': False,\n            'limit': limit,\n            'offset': 0,\n        }, params)\n        active = self.safe_bool(options, 'active', True)\n        if self.safe_value(params, 'closed') is None:\n            request['closed'] = not active\n        offset = self.safe_integer(request, 'offset', 0)\n        markets: List[Any] = []\n        while(True):\n            pageRequest = self.extend(request, {'offset': offset})\n            response = await self.gamma_public_get_markets(pageRequest)\n            page = self.safe_list(response, 'data', response) or []\n            markets = self.array_concat(markets, page)\n            if len(page) < limit:\n                break\n            offset += limit\n        filtered = []\n        for i in range(0, len(markets)):\n            market = markets[i]\n            id = self.safe_string(market, 'id')\n            conditionId = self.safe_string(market, 'conditionId') or self.safe_string(market, 'condition_id')\n            if id is None and conditionId is None:\n                continue\n            filtered.append(market)\n        return self.parse_markets(filtered)\n\n    def parse_market(self, market: dict) -> Market:\n        # Schema uses 'conditionId'(camelCase)\n        conditionId = self.safe_string(market, 'conditionId')\n        question = self.safe_string(market, 'question')\n        # Schema uses 'questionID'(camelCase)\n        questionId = self.safe_string(market, 'questionID')\n        # Schema uses 'slug'(camelCase)\n        slug = self.safe_string(market, 'slug')\n        active = self.safe_bool(market, 'active', False)\n        closed = self.safe_bool(market, 'closed', False)\n        archived = self.safe_bool(market, 'archived', False)\n        outcomes = []\n        outcomePrices = []\n        outcomesStr = self.safe_string(market, 'outcomes')\n        if outcomesStr is not None:\n            parsedOutcomes = None\n            try:\n                parsedOutcomes = json.loads(outcomesStr)\n            except Exception as e:\n                parsedOutcomes = None\n            if parsedOutcomes is not None and len(parsedOutcomes) is not None:\n                for i in range(0, len(parsedOutcomes)):\n                    outcomes.append(parsedOutcomes[i])\n            else:\n                outcomesArray = outcomesStr.split(',')\n                for i in range(0, len(outcomesArray)):\n                    v = outcomesArray[i].strip()\n                    if v != '':\n                        outcomes.append(v)\n        outcomePricesStr = self.safe_string(market, 'outcomePrices')\n        if outcomePricesStr is not None:\n            parsedPrices = None\n            try:\n                parsedPrices = json.loads(outcomePricesStr)\n            except Exception as e:\n                parsedPrices = None\n            if parsedPrices is not None and len(parsedPrices) is not None:\n                for i in range(0, len(parsedPrices)):\n                    outcomePrices.append(self.parse_number(parsedPrices[i]))\n            else:\n                pricesArray = outcomePricesStr.split(',')\n                for i in range(0, len(pricesArray)):\n                    v = pricesArray[i].strip()\n                    if v != '':\n                        outcomePrices.append(self.parse_number(v))\n        # Use slug symbol if available\n        baseId = slug or conditionId\n        quoteId = self.safe_string(self.options, 'defaultCollateral', 'USDC')  # Polymarket uses USDC currency\n        # Market type - Polymarket is a prediction market platform\n        marketType: MarketType = 'option'  # Using 'option' match for prediction markets\n        ammType = self.safe_string(market, 'ammType')\n        # Schema uses 'enableOrderBook'(camelCase)\n        enableOrderBook = self.safe_bool(market, 'enableOrderBook', False)\n        # Market metadata\n        category = self.safe_string(market, 'category')\n        description = self.safe_string(market, 'description')\n        tags = self.safe_value(market, 'tags', [])\n        # Schema uses 'clobTokenIds'(camelCase) - can be string or array\n        clobTokenIds = self.safe_value(market, 'clobTokenIds')\n        if clobTokenIds is None:\n            clobTokenIds = []\n        if isinstance(clobTokenIds, str):\n            parsed = None\n            try:\n                parsed = json.loads(clobTokenIds)\n            except Exception as e:\n                parsed = None\n            if parsed is not None and parsed != None and len(parsed) is not None:\n                clobTokenIds = []\n                for i in range(0, len(parsed)):\n                    clobTokenIds.append(parsed[i])\n            else:\n                cleaned = clobTokenIds\n                cleaned = cleaned.replace('[', '').replace(']', '').replace('\"', '')\n                clobTokenIdsArray = cleaned.split(',')\n                clobTokenIds = []\n                for i in range(0, len(clobTokenIdsArray)):\n                    v = clobTokenIdsArray[i].strip()\n                    if v != '':\n                        clobTokenIds.append(v)\n        outcomesInfo = []\n        length = len(outcomes)\n        if len(outcomePrices) > length:\n            length = len(outcomePrices)\n        if len(clobTokenIds) > length:\n            length = len(clobTokenIds)\n        for i in range(0, length):\n            outcome = None\n            if i < len(outcomes):\n                outcome = outcomes[i]\n            price = None\n            if i < len(outcomePrices):\n                price = self.parse_number(outcomePrices[i])\n            clobId = None\n            if i < len(clobTokenIds):\n                clobId = clobTokenIds[i]\n            outcomeId = str(i)\n            if clobId is not None:\n                outcomeId = clobId\n            outcomesInfo.append({\n                'id': outcomeId,\n                'name': outcome,\n                'price': price,\n                'clobId': clobId,\n                'assetId': clobId,\n            })\n        # Parse dates - Schema uses 'endDateIso'(preferred) or 'endDate'(fallback)\n        endDateIso = self.safe_string(market, 'endDateIso') or self.safe_string(market, 'endDate')\n        # Schema uses 'createdAt'(camelCase)\n        createdAt = self.safe_string(market, 'createdAt')\n        createdTimestamp = None\n        if createdAt is not None:\n            createdTimestamp = self.parse8601(createdAt)\n        # Volume and liquidity\n        volume = self.safe_string(market, 'volume')\n        volumeNum = self.safe_number(market, 'volumeNum')\n        liquidity = self.safe_string(market, 'liquidity')\n        liquidityNum = self.safe_number(market, 'liquidityNum')\n        feesEnabled = self.safe_bool(market, 'feesEnabled', False)\n        makerBaseFee = self.safe_number(market, 'makerBaseFee')\n        takerBaseFee = self.safe_number(market, 'takerBaseFee')\n        base = baseId\n        quote = quoteId\n        settle = quote  # Use quote\n        # Parse expiry for option symbol formatting\n        # Handle date-only strings(YYYY-MM-DD) by converting to ISO8601 datetime\n        expiry = None\n        expiryDatetime = endDateIso\n        if endDateIso is not None:\n            dateString = endDateIso\n            # Check if it's a date-only string(YYYY-MM-DD format)\n            if dateString.find(':') < 0:\n                # Append time to make it a valid ISO8601 datetime\n                dateString = dateString + 'T00:00:00Z'\n            expiry = self.parse8601(dateString)\n        # Format symbol with expiry date(similar to binance/okx option format)\n        # Format: base/quote:settle-YYMMDD\n        symbol = base + '/' + quote\n        if expiry is not None:\n            ymd = self.yymmdd(expiry)\n            symbol = symbol + ':' + settle + '-' + ymd\n        # Prediction markets don't have strike prices or option types in the schema\n        # These fields are kept\n        strike = None\n        optionType = None\n        contractSize = self.parse_number('1')\n        # Calculate fees based on feesEnabled flag\n        takerFee = self.parse_number('0')\n        makerFee = self.parse_number('0')\n        if feesEnabled:\n            # Fees are enabled - use makerBaseFee and takerBaseFee from schema\n            # These are typically in basis points(e.g., 200 = 2% = 0.02)\n            if takerBaseFee is not None:\n                takerFee = takerBaseFee / 10000  # Convert basis points to decimal\n            if makerBaseFee is not None:\n                makerFee = makerBaseFee / 10000  # Convert basis points to decimal\n        created = self.milliseconds()  # TODO change it\n        if createdTimestamp is not None:\n            created = createdTimestamp\n        volumeValue = self.parse_number('0')\n        if volumeNum is not None:\n            volumeValue = volumeNum\n        elif volume is not None:\n            volumeValue = self.parse_number(volume)\n        liquidityValue = self.parse_number('0')\n        if liquidityNum is not None:\n            liquidityValue = liquidityNum\n        elif liquidity is not None:\n            liquidityValue = self.parse_number(liquidity)\n        return {\n            'id': conditionId,\n            'symbol': symbol,\n            'base': base,\n            'quote': quote,\n            'settle': settle,\n            'baseId': baseId,\n            'quoteId': quoteId,\n            'settleId': settle,\n            'type': marketType,\n            'spot': False,\n            'margin': False,\n            'swap': False,\n            'future': False,\n            'option': True,  # Prediction markets are treated\n            'active': enableOrderBook and active and not closed and not archived,\n            'contract': True,\n            'linear': None,\n            'inverse': None,\n            'contractSize': contractSize,\n            'expiry': expiry,\n            'expiryDatetime': expiryDatetime,\n            'strike': strike,\n            'optionType': optionType,\n            'taker': takerFee,\n            'maker': makerFee,\n            'precision': {\n                'amount': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n                'price': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n            },\n            'limits': {\n                'leverage': {\n                    'min': None,\n                    'max': None,\n                },\n                'amount': {\n                    'min': None,\n                    'max': None,\n                },\n                'price': {\n                    'min': 0,  # Prediction markets are 0-1\n                    'max': 1,  # Prediction markets are 0-1\n                },\n                'cost': {\n                    'min': None,\n                    'max': None,\n                },\n            },\n            'created': created,\n            'info': self.deep_extend(market, {\n                'outcomes': outcomes,\n                'outcomePrices': outcomePrices,\n                'outcomesInfo': outcomesInfo,\n                'question': question,\n                'slug': slug,\n                'category': category,\n                'description': description,\n                'tags': tags,\n                'condition_id': conditionId,\n                'question_id': questionId,\n                'asset_id': questionId,\n                'ammType': ammType,\n                'enableOrderBook': enableOrderBook,\n                'volume': volumeValue,\n                'liquidity': liquidityValue,\n                'endDateIso': endDateIso,\n                'createdAt': createdAt,\n                'createdTimestamp': createdTimestamp,\n                'clobTokenIds': clobTokenIds,\n                'quoteDecimals': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n                'baseDecimals': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n            }),\n        }\n\n    async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:\n        \"\"\"\n        fetches the order book for a market\n\n        https://docs.polymarket.com/api-reference/orderbook/get-order-book-summary\n\n        :param str symbol: unified symbol of the market to fetch the order book for\n        :param int [limit]: the maximum amount of order book entries to return\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID for the specific outcome(required if market has multiple outcomes)\n        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        request: dict = {}\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a token_id parameter for market ' + symbol)\n        request['token_id'] = tokenId\n        response = await self.clob_public_get_orderbook_token_id(self.extend(request, params))\n        return self.parse_order_book(response, symbol)\n\n    async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks:\n        \"\"\"\n        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getorderbooks\n\n        :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None\n        :param int [limit]: the maximum amount of order book entries to return\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: a dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbol\n        \"\"\"\n        await self.load_markets()\n        if symbols is None:\n            symbols = self.symbols\n        # Build list of token IDs to fetch order books for\n        tokenIds: List[str] = []\n        tokenIdToSymbol: dict = {}\n        for i in range(0, len(symbols)):\n            symbol = symbols[i]\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n                tokenIds.append(tokenId)\n                tokenIdToSymbol[tokenId] = symbol\n        if len(tokenIds) == 0:\n            return {}\n        # Fetch order books for all token IDs at once using POST /books endpoint\n        # See https://docs.polymarket.com/api-reference/orderbook/get-multiple-order-books-summaries-by-request\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: array of order book objects, each with asset_id matching token_id\n        requestBody = []\n        for i in range(0, len(tokenIds)):\n            requestItem: dict = {'token_id': tokenIds[i]}\n            if limit is not None:\n                requestItem['limit'] = limit\n            requestBody.append(requestItem)\n        response = await self.clob_public_post_books(self.extend({'requests': requestBody}, params))\n        # Parse response: array of order book objects, each with asset_id field\n        # Response is directly an array: [{asset_id: \"...\", bids: [...], asks: [...]}, ...]\n        result: dict = {}\n        if isinstance(response, list):\n            for i in range(0, len(response)):\n                orderbookData = response[i]\n                assetId = self.safe_string(orderbookData, 'asset_id')\n                symbol = tokenIdToSymbol[assetId]\n                if symbol is not None:\n                    try:\n                        orderbook = self.parse_order_book(orderbookData, symbol)\n                        result[symbol] = orderbook\n                    except Exception as e:\n                        # Skip markets that fail to parse\n                        continue\n        return result\n\n    def parse_order_book(self, orderbook: dict, symbol: Str = None, timestamp: Int = None, bidsKey: Str = 'bids', asksKey: Str = 'asks', priceKey: Int = 0, amountKey: Int = 1, countOrIdKey: Int = 2) -> OrderBook:\n        # Polymarket CLOB orderbook format(from /book endpoint)\n        # See https://docs.polymarket.com/developers/CLOB/clients/methods-public#getorderbook\n        # {\n        #   \"market\": \"string\",\n        #   \"asset_id\": \"string\",\n        #   \"timestamp\": \"string\",\n        #   \"bids\": [\n        #     {\n        #       \"price\": \"0.65\",  # string\n        #       \"size\": \"100\"     # string\n        #     }\n        #   ],\n        #   \"asks\": [\n        #     {\n        #       \"price\": \"0.66\",  # string\n        #       \"size\": \"50\"      # string\n        #     }\n        #   ],\n        #   \"min_order_size\": \"string\",\n        #   \"tick_size\": \"string\",\n        #   \"neg_risk\": boolean,\n        #   \"hash\": \"string\"\n        # }\n        # Note: Ensure bids and asks are always arrays to avoid Python transpilation issues\n        # safeList can return None, which becomes None in Python, causing len() to fail\n        bids = self.safe_list(orderbook, 'bids', []) or []\n        asks = self.safe_list(orderbook, 'asks', []) or []\n        # Note: Using 'const' without explicit type annotation to avoid Python transpilation issues\n        # The transpiler incorrectly preserves TypeScript tuple type annotations(e.g., ': [number, number][]') in Python code\n        parsedBids = []\n        parsedAsks = []\n        for i in range(0, len(bids)):\n            bid = bids[i]\n            price = self.safe_number(bid, 'priceNumber', self.safe_number(bid, 'price'))\n            amount = self.safe_number(bid, 'sizeNumber', self.safe_number(bid, 'size'))\n            if price is not None and amount is not None:\n                parsedBids.append([price, amount])\n        for i in range(0, len(asks)):\n            ask = asks[i]\n            price = self.safe_number(ask, 'priceNumber', self.safe_number(ask, 'price'))\n            amount = self.safe_number(ask, 'sizeNumber', self.safe_number(ask, 'size'))\n            if price is not None and amount is not None:\n                parsedAsks.append([price, amount])\n        # Extract timestamp from orderbook response if available\n        orderbookTimestamp = self.safe_string(orderbook, 'timestamp')\n        finalTimestamp = timestamp\n        if orderbookTimestamp is not None:\n            # CLOB API returns timestamp string, convert to milliseconds\n            finalTimestamp = self.parse8601(orderbookTimestamp)\n        # Extract tick_size and neg_risk from orderbook if available(useful metadata)\n        # These are also available via get_tick_size() and get_neg_risk() endpoints\n        # Based on py-clob-client: get_tick_size() and get_neg_risk()\n        tickSize = self.safe_string(orderbook, 'tick_size')\n        negRisk = self.safe_bool(orderbook, 'neg_risk')\n        minOrderSize = self.safe_string(orderbook, 'min_order_size')\n        result: OrderBook = {\n            'symbol': symbol,\n            'bids': parsedBids,\n            'asks': parsedAsks,\n            'timestamp': finalTimestamp,\n            'datetime': self.iso8601(finalTimestamp),\n            'nonce': None,\n        }\n        # Include tick_size, neg_risk, and min_order_size in info if available(useful metadata)\n        if tickSize is not None or negRisk is not None or minOrderSize is not None:\n            metadata: dict = {}\n            if tickSize is not None:\n                metadata['tick_size'] = tickSize\n            if negRisk is not None:\n                metadata['neg_risk'] = negRisk\n            if minOrderSize is not None:\n                metadata['min_order_size'] = minOrderSize\n            result['info'] = self.extend(orderbook, metadata)\n        return result\n\n    async def fetch_ticker(self, symbol: str, params={}) -> Ticker:\n        \"\"\"\n        fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market\n\n        https://docs.polymarket.com/api-reference/pricing/get-market-price\n        https://docs.polymarket.com/api-reference/pricing/get-midpoint-price\n\n        :param str symbol: unified symbol of the market to fetch the ticker for\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID for the specific outcome(required if market has multiple outcomes)\n        :param str [params.side]: the side: 'BUY' or 'SELL'(default: 'BUY')\n        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`\n\n **Currently Populated Fields:**\n - `bid` - Best bid price from POST /prices endpoint(BUY side)\n - `ask` - Best ask price from POST /prices endpoint(SELL side)\n - `last` - Midpoint price from GET /midpoint or lastTradePrice from market info\n - `open` - Calculated approximation: last / (1 + oneDayPriceChange)\n - `change` - Calculated: last - open\n - `percentage` - From oneDayPriceChange * 100(from market info)\n - `volume` - From volumeNum or volume(from market info)\n - `timestamp` - From updatedAt(parsed from ISO string)\n - `datetime` - ISO8601 formatted timestamp\n\n **Currently Undefined Fields(Available via Additional API Calls):**\n - `high` - Can be fetched from GET /prices-history(24h price history) or GET /trades(24h trades)\n - `low` - Can be fetched from GET /prices-history(24h price history) or GET /trades(24h trades)\n - `bidVolume` - Can be calculated from GET /book(order book) by summing all bid sizes\n - `askVolume` - Can be calculated from GET /book(order book) by summing all ask sizes\n - `vwap` - Can be calculated from GET /trades(24h trades) using volume-weighted average\n - `average` - Not available\n - `indexPrice` - Not available\n - `markPrice` - Not available\n\n **Enhancement Options:**\n\n 1. **For High/Low/More Accurate Open:**\n    - Use fetchOHLCV() to get 24h price history: `await exchange.fetchOHLCV(symbol, '1h', since24hAgo, None, {token_id: tokenId})`\n    - Calculate high/low from OHLCV data\n    - Use first candle's open price for accurate 24h open\n    - API: GET /prices-history(see https://docs.polymarket.com/developers/CLOB/timeseries)\n\n 2. **For VWAP:**\n    - Use fetchTrades() to get 24h trades: `await exchange.fetchTrades(symbol, since24hAgo, None, {token_id: tokenId})`\n    - Calculate: vwap = sum(trade.cost) / sum(trade.amount)\n    - API: GET /trades(see https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets)\n\n 3. **For Bid/Ask Volumes:**\n    - Use fetchOrderBook() to get order book: `await exchange.fetchOrderBook(symbol, None, {token_id: tokenId})`\n    - Calculate: bidVolume = sum of all bid[1](sizes), askVolume = sum of all ask[1](sizes)\n    - API: GET /book(see https://docs.polymarket.com/api-reference/orderbook/get-order-book-summary)\n\n 4. **For More Accurate Last Price:**\n    - Use GET /last-trade-price endpoint: `await exchange.clobPublicGetLastTradePrice({token_id: tokenId})`\n    - API: GET /last-trade-price(see https://docs.polymarket.com/api-reference/trades/get-last-trade-price)\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchTicker() requires a token_id parameter for market ' + symbol)\n        # Fetch prices using POST /prices endpoint with both BUY and SELL sides\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        pricesResponse = await self.clob_public_post_prices(self.extend({\n            'requests': [\n                {'token_id': tokenId, 'side': 'BUY'},\n                {'token_id': tokenId, 'side': 'SELL'},\n            ],\n        }, params))\n        # Parse prices response: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        tokenPrices = self.safe_dict(pricesResponse, tokenId, {})\n        buyPrice = self.safe_string(tokenPrices, 'BUY')\n        sellPrice = self.safe_string(tokenPrices, 'SELL')\n        # Fetch midpoint if available(optional, ignore if not provided)\n        midpoint = None\n        try:\n            midpointResponse = await self.clob_public_get_midpoint(self.extend({'token_id': tokenId}, params))\n            midpoint = self.safe_string(midpointResponse, 'mid')\n        except Exception as e:\n            # Ignore midpoint if not available or fails\n            midpoint = None\n        # Combine pricing data with market info - already loaded from fetchMarkets\n        combinedData = self.deep_extend(marketInfo, {\n            'buyPrice': buyPrice,\n            'sellPrice': sellPrice,\n            'midpoint': midpoint,\n        })\n        return self.parse_ticker(combinedData, market)\n\n    async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:\n        \"\"\"\n        fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market\n\n        https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices\n\n        :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param boolean [params.fetchSpreads]: if True, also fetch bid-ask spreads for all markets(default: False)\n        :returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Build list of token IDs to fetch prices for\n        tokenIds: List[str] = []\n        tokenIdToSymbol: dict = {}\n        symbolsToFetch = symbols or self.symbols\n        for i in range(0, len(symbolsToFetch)):\n            symbol = symbolsToFetch[i]\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n                tokenIds.append(tokenId)\n                tokenIdToSymbol[tokenId] = symbol\n        if len(tokenIds) == 0:\n            return {}\n        # Build requests array for POST /prices endpoint\n        # Each token needs both BUY and SELL sides\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        requests = []\n        for i in range(0, len(tokenIds)):\n            tokenId = tokenIds[i]\n            requests.append({'token_id': tokenId, 'side': 'BUY'})\n            requests.append({'token_id': tokenId, 'side': 'SELL'})\n        # Fetch prices for all token IDs at once using POST /prices endpoint\n        # Response format: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        pricesResponse = await self.clob_public_post_prices(self.extend({'requests': requests}, params))\n        # Optionally fetch spreads for all token IDs\n        # See https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spreads\n        fetchSpreads = self.safe_bool(params, 'fetchSpreads', False)\n        spreadsResponse = {}\n        if fetchSpreads:\n            try:\n                spreadsResponse = await self.clob_public_post_spreads(self.extend({'token_ids': tokenIds}, params))\n            except Exception as e:\n                spreadsResponse = {}\n        # Build market data map for efficient lookup\n        tokenIdToMarket = {}\n        for i in range(0, len(tokenIds)):\n            tokenId = tokenIds[i]\n            symbol = tokenIdToSymbol[tokenId]\n            tokenIdToMarket[tokenId] = self.market(symbol)\n        # Parse prices and build tickers(no additional fetching during parsing)\n        tickers: dict = {}\n        for i in range(0, len(tokenIds)):\n            tokenId = tokenIds[i]\n            symbol = tokenIdToSymbol[tokenId]\n            market = tokenIdToMarket[tokenId]\n            try:\n                # Get prices from the response(both BUY and SELL are in the same response)\n                tokenPrices = self.safe_dict(pricesResponse, tokenId, {})\n                buyPrice = self.safe_string(tokenPrices, 'BUY')\n                sellPrice = self.safe_string(tokenPrices, 'SELL')\n                # Get spread if available\n                spread = self.safe_string(spreadsResponse, tokenId)\n                # Use market info data(already loaded from fetchMarkets)\n                marketInfo = self.safe_dict(market, 'info', {})\n                # Combine pricing data with market info\n                combinedData = self.deep_extend(marketInfo, {\n                    'buyPrice': buyPrice,\n                    'sellPrice': sellPrice,\n                    'spread': spread,\n                })\n                ticker = self.parse_ticker(combinedData, market)\n                tickers[symbol] = ticker\n            except Exception as e:\n                # Skip markets that fail to parse\n                continue\n        return tickers\n\n    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:\n        \"\"\"\n        parses a ticker data structure from Polymarket API response\n        :param dict ticker: ticker data structure from Polymarket API\n        :param dict [market]: market structure\n        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`\n\n **Data Sources:**\n - Market info from fetchMarkets()(volume, oneDayPriceChange, lastTradePrice, etc.)\n - Pricing API(buyPrice, sellPrice, midpoint)\n - Market metadata(updatedAt, volume24hr, volume1wk, volume1mo, volume1yr)\n\n **Currently Parsed Fields:**\n - `bid` - From buyPrice(POST /prices BUY side) or bestBid(market info)\n - `ask` - From sellPrice(POST /prices SELL side) or bestAsk(market info)\n - `last` - From midpoint(GET /midpoint) or lastTradePrice(market info)\n - `open` - Calculated: last / (1 + oneDayPriceChange) when both available\n - `change` - Calculated: last - open\n - `percentage` - From oneDayPriceChange * 100\n - `volume` - From volumeNum or volume(market info)\n - `timestamp` - From updatedAt(ISO string parsed to milliseconds)\n - `datetime` - ISO8601 formatted timestamp\n\n **Fields Set to Undefined(Can Be Enhanced):**\n - `high` - Not available in current data sources. Can be calculated from:\n   - Price history: Math.max(...ohlcvData.map(c => c[2])) where c[2] is high\n   - Trades: Math.max(...trades.map(t => t.price))\n - `low` - Not available in current data sources. Can be calculated from:\n   - Price history: Math.min(...ohlcvData.map(c => c[3])) where c[3] is low\n   - Trades: Math.min(...trades.map(t => t.price))\n - `bidVolume` - Not available. Can be calculated from order book:\n   - orderbook.bids.reduce((sum, bid) => sum + bid[1], 0)\n - `askVolume` - Not available. Can be calculated from order book:\n   - orderbook.asks.reduce((sum, ask) => sum + ask[1], 0)\n - `vwap` - Not available. Can be calculated from trades:\n   - totalCost = trades.reduce((sum, t) => sum + t.cost, 0)\n   - totalVolume = trades.reduce((sum, t) => sum + t.amount, 0)\n   - vwap = totalCost / totalVolume\n\n **To Enhance Ticker Data:**\n Before calling parseTicker(), you can fetch additional data and add it to the ticker dict:\n\n ```typescript\n  # Example: Add high/low from price history\n since24h = exchange.milliseconds() - 24 * 60 * 60 * 1000\n ohlcv = await exchange.fetchOHLCV(symbol, '1h', since24h, None, {token_id: tokenId})\n if len(ohlcv) > 0:\n     highs = ohlcv.map(c => c[2])  # OHLCV[2] is high\n     lows = ohlcv.map(c => c[3])  # OHLCV[3] is low\n     ticker['high'] = Math.max(...highs)\n     ticker['low'] = Math.min(...lows)\n     ticker['open'] = ohlcv[0][1]  # First candle's open\n}\n\n  # Example: Add VWAP from trades\n trades = await exchange.fetchTrades(symbol, since24h, None, {token_id: tokenId})\n if len(trades) > 0:\n     totalCost = 0\n     totalVolume = 0\n     for i in range(0, len(trades)):\n         totalCost += trades[i]['cost']\n         totalVolume += trades[i]['amount']\n     }\n     ticker['vwap'] = totalVolume > totalCost / totalVolume if 0 else None\n}\n\n  # Example: Add bid/ask volumes from order book\n orderbook = await exchange.fetchOrderBook(symbol, None, {token_id: tokenId})\n bidVolume = 0\n askVolume = 0\n for i in range(0, len(orderbook['bids'])):\n     bidVolume += orderbook['bids'][i][1]\n}\n for i in range(0, len(orderbook['asks'])):\n     askVolume += orderbook['asks'][i][1]\n}\n ticker['bidVolume'] = bidVolume\n ticker['askVolume'] = askVolume\n ```\n        \"\"\"\n        # Polymarket ticker format from market data\n        symbol = market['symbol'] if market else None\n        # Parse outcome prices\n        outcomePricesStr = self.safe_string(ticker, 'outcomePrices')\n        outcomePrices = []\n        if outcomePricesStr:\n            try:\n                parsed = json.loads(outcomePricesStr)\n                # Note: Ensure all elements are numbers - json.loadsmay return strings\n                # Convert each element to a number to avoid Python multiplication errors\n                if parsed is not None and parsed != None and len(parsed) is not None:\n                    for i in range(0, len(parsed)):\n                        price = self.parse_number(parsed[i])\n                        if price is not None:\n                            outcomePrices.append(price)\n            except Exception as e:\n                # Note: Using for loop instead of .map() to avoid Python transpilation issues\n                # Arrow functions with type annotations(e.g., '(p: string) =>') are incorrectly preserved in Python\n                pricesArray = outcomePricesStr.split(',')\n                for i in range(0, len(pricesArray)):\n                    price = self.parse_number(pricesArray[i].strip())\n                    if price is not None:\n                        outcomePrices.append(price)\n        last = None\n        bid = None\n        ask = None\n        high = None\n        low = None\n        # Volume data\n        volume = self.safe_number(ticker, 'volumeNum', self.safe_number(ticker, 'volume'))\n        volume24hr = self.safe_number(ticker, 'volume24hr')\n        volume1wk = self.safe_number(ticker, 'volume1wk')\n        volume1mo = self.safe_number(ticker, 'volume1mo')\n        volume1yr = self.safe_number(ticker, 'volume1yr')\n        # Price changes\n        oneDayPriceChange = self.safe_number(ticker, 'oneDayPriceChange')\n        # Best bid/ask from pricing API(BUY = bid, SELL = ask)\n        buyPrice = self.safe_number(ticker, 'buyPrice')\n        sellPrice = self.safe_number(ticker, 'sellPrice')\n        midpoint = self.safe_number(ticker, 'midpoint')\n        # Use pricing API data if available\n        if buyPrice is not None:\n            bid = buyPrice\n        if sellPrice is not None:\n            ask = sellPrice\n        if midpoint is not None:\n            last = midpoint\n        # Fallback to ticker data if pricing API data not available\n        bestBid = self.safe_number(ticker, 'bestBid')\n        bestAsk = self.safe_number(ticker, 'bestAsk')\n        lastTradePrice = self.safe_number(ticker, 'lastTradePrice')\n        if bid is None and bestBid is not None:\n            bid = bestBid\n        if ask is None and bestAsk is not None:\n            ask = bestAsk\n        if last is None and lastTradePrice is not None:\n            last = lastTradePrice\n        # Timestamp\n        updatedAtString = self.safe_string(ticker, 'updatedAt')\n        timestamp = self.parse8601(updatedAtString) if updatedAtString else None\n        datetime = self.iso8601(timestamp) if timestamp else None\n        # Open(previous closing price - approximated)\n        open = last is not None and oneDayPriceChange is not last / (1 + oneDayPriceChange) if None else None\n        # Change and percentage\n        change = last is not None and open is not last - open if None else None\n        percentage = oneDayPriceChange is not oneDayPriceChange * 100 if None else None\n        # Add additional Polymarket-specific fields to info\n        tickerInfo = self.safe_dict(ticker, 'info', {})\n        extendedInfo = self.deep_extend(tickerInfo, {\n            'buyPrice': buyPrice,\n            'sellPrice': sellPrice,\n            'midpoint': midpoint,\n            'lastTradePrice': lastTradePrice,\n            'volume24hr': volume24hr,\n            'volume1wk': volume1wk,\n            'volume1mo': volume1mo,\n            'volume1yr': volume1yr,\n        })\n        return {\n            'symbol': symbol,\n            'info': self.deep_extend(ticker, {'info': extendedInfo}),\n            'timestamp': timestamp,\n            'datetime': datetime,\n            'high': high,\n            'low': low,\n            'bid': bid,\n            'bidVolume': None,\n            'ask': ask,\n            'askVolume': None,\n            'vwap': None,\n            'open': open,\n            'close': last,\n            'last': last,\n            'previousClose': open,\n            'change': change,\n            'percentage': percentage,\n            'average': None,\n            'baseVolume': volume,\n            'quoteVolume': volume,\n            'indexPrice': None,\n            'markPrice': None,\n        }\n\n    async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        get the list of most recent trades for a particular symbol\n\n        https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n\n        :param str symbol: unified symbol of the market to fetch trades for\n        :param int [since]: timestamp in ms of the earliest trade to fetch\n        :param int [limit]: the maximum amount of trades to fetch(default: 100, max: 10000)\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.offset]: offset for pagination(default: 0, max: 10000)\n        :param boolean [params.takerOnly]: if True, returns only trades where the user is the taker(default: True)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get condition_id from market info(self is the market ID for Polymarket)\n        conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n        request: dict = {\n            'market': [conditionId],  # Data API expects an array of condition IDs\n        }\n        # Note: Data API /trades endpoint supports limit(default: 100, max: 10000) and offset for pagination\n        # The 'since' parameter is not directly supported by the REST API\n        if limit is not None:\n            request['limit'] = min(limit, 10000)  # Cap at max 10000\n        offset = self.safe_integer(params, 'offset')\n        if offset is not None:\n            request['offset'] = offset\n        takerOnly = self.safe_bool(params, 'takerOnly', True)\n        request['takerOnly'] = takerOnly\n        side = self.safe_string_upper(params, 'side')\n        if side is not None:\n            request['side'] = side\n        response = await self.data_public_get_trades(self.extend(request, self.omit(params, ['offset', 'takerOnly', 'side'])))\n        tradesData = []\n        if isinstance(response, list):\n            tradesData = response\n        else:\n            dataList = self.safe_list(response, 'data', [])\n            if dataList is not None:\n                tradesData = dataList\n        return self.parse_trades(tradesData, market, since, limit)\n\n    def parse_trade(self, trade: dict, market: Market = None) -> Trade:\n        # Detect Data API format(has conditionId field) vs CLOB format(has market/asset_id fields)\n        # Check for both camelCase and snake_case for robustness\n        conditionId = self.safe_string_2(trade, 'conditionId', 'condition_id')\n        isDataApiFormat = conditionId is not None\n        if isDataApiFormat:\n            # Data API format: https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n            # {\n            #   \"proxyWallet\": \"0x...\",\n            #   \"side\": \"BUY\",\n            #   \"asset\": \"<string>\",\n            #   \"conditionId\": \"0x...\",\n            #   \"size\": 123,\n            #   \"price\": 123,\n            #   \"timestamp\": 123,\n            #   \"transactionHash\": \"0x...\",\n            #   ...\n            # }\n            # Use transactionHash, check both camelCase and snake_case\n            id = self.safe_string_2(trade, 'transactionHash', 'transaction_hash')\n            symbol = None\n            if market is not None and market['symbol'] is not None:\n                symbol = market['symbol']\n            elif conditionId is not None:\n                resolved = self.safe_market(conditionId, None)\n                resolvedSymbol = self.safe_string(resolved, 'symbol')\n                if resolvedSymbol is not None:\n                    symbol = resolvedSymbol\n                else:\n                    symbol = conditionId\n            timestampSeconds = self.safe_integer(trade, 'timestamp')\n            timestamp = None\n            if timestampSeconds is not None:\n                timestamp = timestampSeconds * 1000\n            side = self.safe_string_lower(trade, 'side')\n            price = self.safe_number(trade, 'price')\n            amount = self.safe_number(trade, 'size')\n            cost = None\n            if price is not None and amount is not None:\n                cost = price * amount\n            # Data API doesn't provide fee information\n            return {\n                'id': id,\n                'info': trade,\n                'timestamp': timestamp,\n                'datetime': self.iso8601(timestamp),\n                'symbol': symbol,\n                'type': None,\n                'side': side,\n                'takerOrMaker': None,  # Data API doesn't provide self information\n                'price': price,\n                'amount': amount,\n                'cost': cost,\n                'fee': None,  # Data API doesn't provide fee information\n                'order': None,  # Data API doesn't provide order ID\n            }\n        else:\n            # CLOB Trade format(backward compatibility)\n            # interface Trade {\n            #   id: string\n            #   taker_order_id: string\n            #   market: string\n            #   asset_id: string\n            #   side: Side\n            #   size: string\n            #   fee_rate_bps: string\n            #   price: string\n            #   status: string\n            #   match_time: string\n            #   last_update: string\n            #   outcome: string\n            #   bucket_index: number\n            #   owner: string\n            #   maker_address: string\n            #   maker_orders: MakerOrder[]\n            #   transaction_hash: string\n            #   trader_side: \"TAKER\" | \"MAKER\"\n            # }\n            id = self.safe_string(trade, 'id')\n            assetId = self.safe_string(trade, 'asset_id')\n            tradeMarket = self.safe_string(trade, 'market')\n            symbol = None\n            if market is not None and market['symbol'] is not None:\n                symbol = market['symbol']\n            elif tradeMarket is not None:\n                resolved = self.safe_market(tradeMarket, None)\n                resolvedSymbol = self.safe_string(resolved, 'symbol')\n                if resolvedSymbol is not None:\n                    symbol = resolvedSymbol\n                else:\n                    symbol = tradeMarket\n            elif assetId is not None:\n                resolved = self.safe_market(assetId, market)\n                resolvedSymbol = self.safe_string(resolved, 'symbol')\n                if resolvedSymbol is not None:\n                    symbol = resolvedSymbol\n                else:\n                    symbol = assetId\n            matchTime = self.safe_integer(trade, 'match_time')\n            timestamp = None\n            if matchTime is not None:\n                timestamp = matchTime * 1000\n            # Top-level fields are from the taker perspective; for maker trades use maker_orders\n            side = self.safe_string_lower(trade, 'side')\n            price = self.safe_number(trade, 'price')\n            amount = self.safe_number(trade, 'size')\n            feeRateBps = self.safe_number(trade, 'fee_rate_bps')\n            traderSide = self.safe_string_upper(trade, 'trader_side')\n            if traderSide == 'MAKER':\n                makerOrders = self.safe_value(trade, 'maker_orders', [])\n                proxyWallet = self.get_proxy_wallet_address()\n                userAddress = proxyWallet.lower()\n                matched = False\n                for i in range(0, len(makerOrders)):\n                    m = makerOrders[i]\n                    mAddr = self.safe_string(m, 'maker_address')\n                    if mAddr is not None:\n                        mAddrLower = mAddr.lower()\n                        if mAddrLower == userAddress:\n                                price = self.safe_number(m, 'price')\n                                amount = self.safe_number(m, 'matched_amount')\n                                side = self.safe_string_lower(m, 'side')\n                                feeRateBps = self.safe_number(m, 'fee_rate_bps')\n                                matched = True\n                                break\n                if not matched:\n                    m = makerOrders[0]\n                    price = self.safe_number(m, 'price')\n                    amount = self.safe_number(m, 'matched_amount')\n                    side = self.safe_string_lower(m, 'side')\n                    feeRateBps = self.safe_number(m, 'fee_rate_bps')\n            feeCost = None\n            if feeRateBps is not None and price is not None and amount is not None:\n                feeCost = price * amount * feeRateBps / 10000\n            fee = None\n            if feeCost is not None:\n                fee = {\n                    'cost': feeCost,\n                    'currency': self.safe_string(self.options, 'defaultCollateral', 'USDC'),\n                    'rate': feeRateBps is not feeRateBps / 10000 if None else None,\n                }\n            cost = price * amount if (price is not None and amount is not None) else None\n            return {\n                'id': id,\n                'info': trade,\n                'timestamp': timestamp,\n                'datetime': self.iso8601(timestamp),\n                'symbol': symbol,\n                'type': None,\n                'side': side,\n                'takerOrMaker': self.safe_string_lower(trade, 'trader_side'),\n                'price': price,\n                'amount': amount,\n                'cost': cost,\n                'fee': fee,\n                'order': self.safe_string(trade, 'taker_order_id'),\n            }\n\n    async def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}) -> List[list]:\n        \"\"\"\n        fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getpriceshistory\n\n        :param str symbol: unified symbol of the market to fetch OHLCV data for\n        :param str timeframe: the length of time each candle represents\n        :param int [since]: timestamp in ms of the earliest candle to fetch\n        :param int [limit]: the maximum amount of candles to fetch\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID for the specific outcome(required if market has multiple outcomes)\n        :param int [params.endTs]: timestamp in seconds for the ending date filter\n        :param number [params.fidelity]: data fidelity/quality\n        :returns int[][]: A list of candles ordered, open, high, low, close, volume\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        request: dict = {}\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a token_id parameter for market ' + symbol)\n        request['market'] = tokenId  # API uses 'market' parameter for token_id\n        # Note: REST API /prices-history endpoint requires either:\n        # 1. startTs and endTs(mutually exclusive with interval)\n        # 2. interval(mutually exclusive with startTs/endTs)\n        # See https://docs.polymarket.com/developers/CLOB/timeseries\n        # Supported intervals: \"1m\", \"1h\", \"6h\", \"1d\", \"1w\", \"max\"\n        # CCXT will automatically reject unsupported timeframes based on the 'timeframes' definition\n        endTs = self.safe_integer(params, 'endTs')\n        if since is not None or endTs is not None:\n            # Use startTs/endTs when time range is specified\n            if since is not None:\n                # Convert milliseconds to seconds for API\n                request['startTs'] = self.parse_to_int(since / 1000)\n            if endTs is not None:\n                request['endTs'] = endTs\n        else:\n            # Use interval when no time range is specified\n            # CCXT will validate the timeframe against the 'timeframes' definition\n            # Map to API format(timeframe should already be validated by CCXT)\n            request['interval'] = timeframe\n        # Fidelity parameter controls data granularity(e.g., 720 for 12-hour intervals)\n        # If not provided, API may use default fidelity\n        fidelity = self.safe_number(params, 'fidelity')\n        # Polymarket enforces minimum fidelity per interval(e.g. interval=1m requires fidelity>=10)\n        # Avoid leaking a server-side validation error back to the user when a too-low value is supplied.\n        if timeframe == '1m':\n            if fidelity is None:\n                fidelity = 10\n            else:\n                fidelity = min(10, fidelity)\n        if fidelity is not None:\n            request['fidelity'] = fidelity\n        remainingParams = self.omit(params, ['token_id', 'endTs', 'fidelity'])\n        response = await self.clob_public_get_prices_history(self.extend(request, remainingParams))\n        ohlcvData = []\n        if isinstance(response, list):\n            ohlcvData = response\n        else:\n            # Response has 'history' key containing the array\n            ohlcvData = self.safe_list(response, 'history', []) or []\n        return self.parse_ohlcvs(ohlcvData, market, timeframe, since, limit)\n\n    def parse_ohlcv(self, ohlcv: Any, market: Market = None) -> list:\n        # Polymarket MarketPrice format from getPricesHistory\n        # See https://docs.polymarket.com/developers/CLOB/clients/methods-public#getpriceshistory\n        # {\n        #   \"t\": number,  # timestamp in seconds\n        #   \"p\": number   # price\n        # }\n        # Note: Polymarket only returns price data, not full OHLCV\n        # We'll use the price, high, low, and close, with volume\n        timestamp = self.safe_integer(ohlcv, 't')\n        price = self.safe_number(ohlcv, 'p')\n        # Convert timestamp from seconds to milliseconds\n        if timestamp is not None:\n            timestamp = timestamp * 1000\n        return [\n            timestamp,\n            price,  # open\n            price,  # high(same since we only have price)\n            price,  # low(same since we only have price)\n            price,  # close\n            0,     # volume(not available in price history)\n        ]\n\n    def get_rounding_config(self, tickSize: str) -> dict:\n        \"\"\"\n        Get rounding configuration based on tick size(matches ROUNDING_CONFIG from official SDK)\n        :param str tickSize: tick size string(e.g., '0.1', '0.01', '0.001', '0.0001')\n        :returns dict: rounding configuration with price, size, and amount decimal places\n        \"\"\"\n        # Determine rounding config based on tick size(matches ROUNDING_CONFIG from SDK)\n        # Returns: {price: number, size: number, amount: number}\n        priceDecimals = 2\n        sizeDecimals = 2\n        amountDecimals = 4\n        if tickSize == '0.1':\n            priceDecimals = 1\n            sizeDecimals = 2\n            amountDecimals = 3\n        elif tickSize == '0.01':\n            priceDecimals = 2\n            sizeDecimals = 2\n            amountDecimals = 4\n        elif tickSize == '0.001':\n            priceDecimals = 3\n            sizeDecimals = 2\n            amountDecimals = 5\n        elif tickSize == '0.0001':\n            priceDecimals = 4\n            sizeDecimals = 2\n            amountDecimals = 6\n        return {\n            'price': priceDecimals,\n            'size': sizeDecimals,\n            'amount': amountDecimals,\n        }\n\n    def round_down(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Round down(truncate) a value to specific decimal places\n        :param str value: value to round down\n        :param number decimals: number of decimal places\n        :returns str: rounded down value\n        \"\"\"\n        return self.decimal_to_precision(value, 0, decimals, 2, 5)\n\n    def round_normal(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Round a value normally to specific decimal places\n        :param str value: value to round\n        :param number decimals: number of decimal places\n        :returns str: rounded value\n        \"\"\"\n        return self.decimal_to_precision(value, 1, decimals, 2, 5)\n\n    def round_up(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Round up a value to specific decimal places\n        :param str value: value to round up\n        :param number decimals: number of decimal places\n        :returns str: rounded up value\n        \"\"\"\n        return self.decimal_to_precision(value, 2, decimals, 2, 5)\n\n    def decimal_places(self, value: str) -> float:\n        \"\"\"\n        Count the number of decimal places in a string value\n        :param str value: value to count decimal places for\n        :returns number: number of decimal places\n        \"\"\"\n        parts = value.split('.')\n        if len(parts) == 2:\n            return len(parts[1])\n        return 0\n\n    def to_token_decimals(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Convert a value to token decimals(smallest unit) by multiplying by 10^decimals and truncating\n        :param str value: value to convert\n        :param number decimals: number of decimals(e.g., 6 for USDC, 18 for tokens)\n        :returns str: value in smallest unit\n        \"\"\"\n        # Multiply by 10^decimals and truncate to integer\n        multiplier = self.integer_precision_to_amount(self.number_to_string(-decimals))\n        return Precise.string_div(Precise.string_mul(value, multiplier), '1', 0)\n\n    async def build_and_sign_order(self, tokenId: str, side: str, size: str, price: str = None, market: Market = None, params={}) -> dict:\n        \"\"\"\n        Builds and signs an order with EIP-712 according to Polymarket order-utils specification\n\n        https://github.com/Polymarket/clob-order-utils\n        https://github.com/Polymarket/clob-client/blob/main/src/order-builder/builder.ts\n        https://github.com/Polymarket/python-order-utils/blob/main/py_order_utils/builders/order_builder.py\n\n        :param str tokenId: the token ID\n        :param str side: 'BUY' or 'SELL'\n        :param str size: order size\n        :param str [price]: order price(required for limit orders)\n        :param dict [market]: market structure(optional, used to get fees)\n        :param dict [params]: extra parameters\n        :param number [params.expiration]: expiration timestamp in seconds(default: 30 days from now)\n        :param number [params.nonce]: order nonce(default: 0)\n        :param number [params.feeRateBps]: fee rate in basis points(default: from market or 200 bps)\n        :param str [params.maker]: maker address(default: getMainWalletAddress())\n        :param str [params.taker]: taker address(default: zero address)\n        :param str [params.signer]: signer address(default: maker address)\n        :param number [params.signatureType]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :param str [params.orderType]: order type: 'GTC', 'IOC', 'FOK', 'GTD'(default: 'GTC' for limit orders, 'FOK' for market orders)\n        :returns dict: signed order object ready for submission\n        \"\"\"\n        # Get zero address constant(matches py-clob-client ZERO_ADDRESS)\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/constants.py\n        zeroAddress = self.safe_string(self.options, 'zeroAddress', '0x0000000000000000000000000000000000000000')\n        # Get signature type\n        signatureType = self.get_signature_type(params)\n        # Get maker address(wallet address) - checksummed for signing\n        maker = self.safe_string(params, 'maker')\n        if maker is None:\n            signatureTypes = self.safe_dict(self.options, 'signatureTypes', {})\n            eoaSignatureType = self.safe_integer(signatureTypes, 'EOA')\n            if signatureType == eoaSignatureType:\n                maker = self.get_main_wallet_address()\n            else:\n                maker = self.get_proxy_wallet_address()\n        normalizedMaker = self.normalize_address(maker)\n        # Get taker address(default: zero address for open orders)\n        taker = self.safe_string(params, 'taker', zeroAddress)\n        normalizedTaker = self.normalize_address(taker)\n        # Get fee rate in basis points from market or params\n        feeRateBps = self.safe_integer(params, 'feeRateBps')\n        if feeRateBps is None:\n            if market is not None:\n                # Try to get fee from market structure\n                marketInfo = self.safe_dict(market, 'info', {})\n                # First try takerBaseFee from market info(in basis points)\n                feeRateBps = self.safe_integer(marketInfo, 'takerBaseFee')\n                if feeRateBps is None:\n                    # Try taker fee from parsed market(decimal, convert to basis points)\n                    takerFee = self.safe_number(market, 'taker')\n                    if takerFee is not None:\n                        feeRateBps = int(round(takerFee * 10000))\n            # Fallback to default fee rate from options if not found in market\n            if feeRateBps is None:\n                feeRateBps = self.safe_integer(self.options, 'defaultFeeRateBps', 200)\n        # Get expiration(default: from options.defaultExpirationDays, or 30 days from now in seconds)\n        expiration = self.safe_integer(params, 'expiration')\n        if expiration is None:\n            nowSeconds = int(math.floor(self.milliseconds()) / 1000)\n            defaultExpirationDays = self.safe_integer(self.options, 'defaultExpirationDays', 30)\n            expiration = nowSeconds + (defaultExpirationDays * 24 * 60 * 60)\n        # Get nonce(default: current timestamp in seconds)\n        nonce = self.safe_integer(params, 'nonce')\n        if nonce is None:\n            nonce = 0  # Default nonce is 0\n        # Get signer address(default: maker address)\n        signer = self.safe_string(params, 'signer')\n        if signer is None:\n            signer = self.get_main_wallet_address()\n        normalizedSigner = self.normalize_address(signer)\n        # Generate salt(unique integer based on microseconds)\n        # Using microseconds for better uniqueness without relying on Math.random()\n        salt = int(math.floor(self.milliseconds()) / 1000)\n        # Calculate makerAmount and takerAmount from size and price\n        # Key steps: 1) Round down size first, 2) Calculate other amount, 3) Round if needed, 4) Convert to smallest units\n        # Get precision from market info or use defaults(USDC: 6 decimals, Tokens: 18 decimals)\n        orderMarketInfo: Any = {}\n        if market is not None:\n            orderMarketInfo = self.safe_dict(market, 'info', {})\n        marketPrecision: Any = {}\n        if market is not None:\n            marketPrecision = self.safe_dict(market, 'precision', {})\n        quoteDecimals = self.safe_integer(orderMarketInfo, 'quoteDecimals', self.safe_integer(marketPrecision, 'price'))\n        baseDecimals = self.safe_integer(orderMarketInfo, 'baseDecimals', self.safe_integer(marketPrecision, 'amount'))\n        defaultTickSize = self.safe_string(self.options, 'defaultTickSize')\n        tickSize = self.safe_string(orderMarketInfo, 'tick_size', defaultTickSize)\n        roundingConfig = self.get_rounding_config(tickSize)\n        priceDecimals = self.safe_integer(roundingConfig, 'price', 2)\n        sizeDecimals = self.safe_integer(roundingConfig, 'size', 2)\n        amountDecimals = self.safe_integer(roundingConfig, 'amount', 4)\n        makerAmount: str\n        takerAmount: str\n        isBuy = (side.upper() == 'BUY')\n        # Get price: from parameter, or from params.marketPrice for market orders\n        orderPrice = price\n        if orderPrice is None:\n            orderPrice = self.safe_string(params, 'marketPrice')\n        if orderPrice is None:\n            raise ArgumentsRequired(self.id + ' buildAndSignOrder() requires a price parameter or params.marketPrice')\n        # Round price and size first, then calculate amounts(same logic for limit and market orders)\n        rawPrice = self.round_normal(orderPrice, priceDecimals)\n        # Check if self is a market order for special decimal handling\n        orderType = self.safe_string(params, 'orderType', 'limit')\n        isMarketOrder = (orderType == 'market')\n        # Get rounding buffer constant\n        roundingBuffer = self.safe_integer(self.options, 'roundingBufferDecimals', 4)\n        # Determine decimal precision based on order type and side\n        makerDecimals = 0\n        takerDecimals = 0\n        if isMarketOrder:\n            # Get market order decimal limits for quote(USDC) and base(tokens)\n            marketOrderQuoteDecimals = self.safe_integer(self.options, 'marketOrderQuoteDecimals', 2)\n            marketOrderBaseDecimals = self.safe_integer(self.options, 'marketOrderBaseDecimals', 4)\n            if isBuy:\n                # Market buy orders: maker gives USDC(quote), taker gives tokens(base)\n                makerDecimals = marketOrderQuoteDecimals\n                takerDecimals = marketOrderBaseDecimals\n            else:\n                # Market sell orders: maker gives tokens(base), taker gives USDC(quote)\n                makerDecimals = marketOrderBaseDecimals\n                takerDecimals = marketOrderQuoteDecimals\n        else:\n            # Limit orders: use amountDecimals for both\n            makerDecimals = amountDecimals\n            takerDecimals = amountDecimals\n        if isBuy:\n            # BUY: maker gives USDC, wants tokens\n            # Round down size first\n            rawTakerAmt = self.round_down(size, sizeDecimals)\n            # Round taker amount to max decimals\n            if self.decimal_places(rawTakerAmt) > takerDecimals:\n                rawTakerAmt = self.round_down(rawTakerAmt, takerDecimals)\n            # Calculate maker amount: raw_maker_amt = raw_taker_amt * raw_price\n            # Do NOT round calculated amounts - preserve full precision for accurate calculations\n            # The decimal limits apply to input size and final representation, not intermediate calculations\n            rawMakerAmt = Precise.string_mul(rawTakerAmt, rawPrice)\n            # Convert to smallest units: maker gives USDC(quoteDecimals), taker gives tokens(baseDecimals)\n            makerAmount = self.to_token_decimals(rawMakerAmt, quoteDecimals)\n            takerAmount = self.to_token_decimals(rawTakerAmt, baseDecimals)\n        else:\n            # SELL: maker gives tokens, wants USDC\n            # Round down size first\n            rawMakerAmt = self.round_down(size, sizeDecimals)\n            # Round maker amount to max decimals\n            if self.decimal_places(rawMakerAmt) > makerDecimals:\n                rawMakerAmt = self.round_down(rawMakerAmt, makerDecimals)\n            # Calculate taker amount: raw_taker_amt = raw_maker_amt * raw_price\n            # Do NOT round calculated amounts - preserve full precision for accurate calculations\n            # The decimal limits apply to input size and final representation, not intermediate calculations\n            rawTakerAmt = Precise.string_mul(rawMakerAmt, rawPrice)\n            # Convert to smallest units: maker gives tokens(baseDecimals), taker gives USDC(quoteDecimals)\n            makerAmount = self.to_token_decimals(rawMakerAmt, baseDecimals)\n            takerAmount = self.to_token_decimals(rawTakerAmt, quoteDecimals)\n        sideInt = self.get_side(side, params)\n        order: dict = {\n            'salt': str(salt),  # uint256\n            'maker': normalizedMaker,  # address\n            'signer': normalizedSigner,  # address\n            'taker': normalizedTaker,  # address\n            'tokenId': str(tokenId),  # uint256\n            'makerAmount': str(makerAmount),  # uint256\n            'takerAmount': str(takerAmount),  # uint256\n            'expiration': str(expiration),  # uint256\n            'nonce': str(nonce),  # uint256\n            'feeRateBps': str(feeRateBps),  # uint256\n            'side': sideInt,  # uint8: number(0 or 1)\n            'signatureType': signatureType,  # uint8: number(0, 1, or 2)\n        }\n        chainId = self.safe_integer(self.options, 'chainId')\n        orderDomainName = self.safe_string(self.options, 'orderDomainName')\n        orderDomainVersion = self.safe_string(self.options, 'orderDomainVersion')\n        contractConfig = self.get_contract_config(chainId)\n        verifyingContract = self.normalize_address(self.safe_string(contractConfig, 'exchange'))\n        # Domain must match exactly what server expects for signature validation\n        domain = {\n            'name': orderDomainName,\n            'version': orderDomainVersion,\n            'chainId': chainId,\n            'verifyingContract': verifyingContract,\n        }\n        # EIP-712 types for orders from https://github.com/Polymarket/clob-order-utils/blob/main/src/exchange.order.const.ts\n        ORDER_STRUCTURE = [\n            {'name': 'salt', 'type': 'uint256'},\n            {'name': 'maker', 'type': 'address'},\n            {'name': 'signer', 'type': 'address'},\n            {'name': 'taker', 'type': 'address'},\n            {'name': 'tokenId', 'type': 'uint256'},\n            {'name': 'makerAmount', 'type': 'uint256'},\n            {'name': 'takerAmount', 'type': 'uint256'},\n            {'name': 'expiration', 'type': 'uint256'},\n            {'name': 'nonce', 'type': 'uint256'},\n            {'name': 'feeRateBps', 'type': 'uint256'},\n            {'name': 'side', 'type': 'uint8'},\n            {'name': 'signatureType', 'type': 'uint8'},\n        ]\n        # primary type is types[0] => 'primaryType': 'Order'\n        # EIP712Domain shouldn't be included in messageTypes\n        messageTypes: dict = {\n            'Order': ORDER_STRUCTURE,\n        }\n        signature = self.sign_typed_data(domain, messageTypes, order)\n        order['signature'] = signature\n        return order\n\n    async def build_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict:\n        \"\"\"\n        build a signed order request payload from order parameters\n\n        https://docs.polymarket.com/developers/CLOB/orders/create-order\n        https://docs.polymarket.com/developers/CLOB/orders/create-order-batch\n\n        :param str symbol: unified symbol of the market to create an order in\n        :param str type: 'market' or 'limit'\n        :param str side: 'buy' or 'sell'\n        :param float amount: how much you want to trade in units of the base currency\n        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required if market has multiple outcomes)\n        :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK', 'GTD'(default: 'GTC')\n        :param str [params.clientOrderId]: a unique id for the order\n        :param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately\n        :param number [params.expiration]: expiration timestamp in seconds(default: 30 days from now)\n        :returns dict: request payload with order, owner, orderType, and optional fields\n        \"\"\"\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' buildOrder() requires a token_id parameter for market ' + symbol)\n        # Convert CCXT side to Polymarket side(BUY/SELL)\n        polymarketSide = 'BUY' if (side == 'buy') else 'SELL'\n        # Convert amount and price to strings\n        size = self.number_to_string(amount)\n        priceStr = None\n        if type == 'limit':\n            if price is None:\n                raise ArgumentsRequired(self.id + ' buildOrder() requires a price parameter for limit orders')\n            priceStr = self.number_to_string(price)\n        elif type == 'market':\n            # For market orders, price is optional but recommended\n            # If not provided, we'll try to fetch from orderbook or use params.marketPrice\n            if price is not None:\n                priceStr = self.number_to_string(price)\n            else:\n                # Try to get price from params.marketPrice\n                marketPrice = self.safe_number(params, 'marketPrice')\n                if marketPrice is not None:\n                    priceStr = self.number_to_string(marketPrice)\n        # Determine orderType(at top level, not inside order object)\n        # Must be determined before building orderObject to set expiration correctly\n        orderType = self.safe_string(params, 'timeInForce', 'GTC')\n        if type == 'market':\n            # For market orders, use IOC(Immediate-Or-Cancel) by default\n            # IOC allows partial fills, making it more forgiving than FOK(Fill-Or-Kill)\n            # Users can still override with params.timeInForce = 'FOK' if needed\n            orderType = self.safe_string(params, 'timeInForce', 'IOC')\n        # Set expiration BEFORE signing: for non-GTD orders(GTC, FOK, FAK), expiration must be '0'\n        # Only GTD orders should have a timestamp expiration\n        # The signature must match the exact expiration value that will be sent to the API\n        # See https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n        orderTypeUpper = orderType.upper()\n        # For non-GTD orders, expiration MUST be '0'(API requirement)\n        # Override any user-provided expiration for non-GTD orders\n        orderParams = self.extend({}, params)\n        if orderTypeUpper == 'GTD':\n            expiration = self.safe_integer(params, 'expiration')\n            if expiration is None:\n                nowSeconds = int(math.floor(self.milliseconds()) / 1000)\n                defaultExpirationDays = self.safe_integer(self.options, 'defaultExpirationDays', 30)\n                expiration = nowSeconds + (defaultExpirationDays * 24 * 60 * 60)\n            else:\n                orderParams['expiration'] = str(expiration)\n        else:\n            # For non-GTD orders, expiration must be 0(will be converted to \"0\" string in signing)\n            orderParams['expiration'] = 0\n        # Pass order type to buildAndSignOrder for market order special handling\n        orderParams['orderType'] = type\n        # Build and sign the order with EIP-712(pass market to use fees from market)\n        signedOrder = await self.build_and_sign_order(tokenId, polymarketSide, size, priceStr, market, orderParams)\n        # override signedOrder types\n        signedOrder['salt'] = self.parse_to_int(signedOrder['salt'])  # integer not string\n        signedOrder['side'] = polymarketSide  # string(BUY or SELL)\n        # Get API credentials for owner field\n        apiCredentials = self.get_api_credentials()\n        owner = self.safe_string(apiCredentials, 'apiKey')\n        if owner is None:\n            raise AuthenticationError(self.id + ' buildOrder() requires API credentials(apiKey)')\n        # Build request payload according to API specification\n        # Top-level fields: order, owner, orderType\n        requestPayload: dict = {\n            'order': signedOrder,\n            'owner': owner,\n            'orderType': orderType.upper(),\n        }\n        # Add optional parameters if provided\n        clientOrderId = self.safe_string(params, 'clientOrderId')\n        if clientOrderId is not None:\n            requestPayload['clientOrderId'] = clientOrderId\n        postOnly = self.safe_bool(params, 'postOnly', False)\n        if postOnly:\n            requestPayload['postOnly'] = True\n        return requestPayload\n\n    async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:\n        \"\"\"\n        create a trade order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/create-order\n        https://github.com/Polymarket/clob-order-utils\n        https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n\n        :param str symbol: unified symbol of the market to create an order in\n        :param str type: 'market' or 'limit'\n        :param str side: 'buy' or 'sell'\n        :param float amount: how much you want to trade in units of the base currency\n        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required if market has multiple outcomes)\n        :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK', 'GTD'(default: 'GTC')\n        :param str [params.clientOrderId]: a unique id for the order\n        :param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately\n        :param number [params.expiration]: expiration timestamp in seconds(default: 30 days from now)\n        :param number [params.nonce]: order nonce(default: current timestamp)\n        :param number [params.feeRateBps]: fee rate in basis points(default: fetched from API)\n        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Build the order request payload\n        requestPayload = await self.build_order(symbol, type, side, amount, price, params)\n        # Extract clientOrderId from request payload for return value\n        clientOrderId = self.safe_string(requestPayload, 'clientOrderId')\n        # Submit order via POST /order endpoint\n        response = await self.clob_private_post_order(self.extend(requestPayload, params))\n        # Response format:\n        # {\n        #     \"success\": boolean,\n        #     \"errorMsg\": string(if error),\n        #     \"orderId\": string,\n        #     \"orderHashes\": string[](if order was marketable)\n        # }\n        success = self.safe_bool(response, 'success', True)\n        if not success:\n            errorMsg = self.safe_string(response, 'errorMsg', 'Unknown error')\n            raise ExchangeError(self.id + ' createOrder() failed: ' + errorMsg)\n        orderId = self.safe_string(response, 'orderID')\n        if orderId is None:\n            raise ExchangeError(self.id + ' createOrder() response missing orderID')\n        market = None\n        if symbol:\n            market = self.market(symbol)\n        # Combine response with order details from requestPayload for parseOrder\n        orderData = self.extend({\n            'orderID': orderId,\n            'clientOrderId': clientOrderId,\n            'order': requestPayload['order'],  # Include the signed order for additional context\n            'order_type': requestPayload['orderType'],  # Include orderType for parseOrder\n        }, response)\n        order = self.parse_order(orderData, market)\n        return order\n\n    async def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]:\n        \"\"\"\n        create multiple trade orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/create-order-batch\n\n        :param Array orders: list of orders to create, each order should contain the parameters required by createOrder\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: an array of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        orderRequests = []\n        clientOrderIds = []\n        symbols = []\n        for i in range(0, len(orders)):\n            order = orders[i]\n            symbol = self.safe_string(order, 'symbol')\n            if symbol is None:\n                raise ArgumentsRequired(self.id + ' createOrders() requires a symbol in each order')\n            type = self.safe_string(order, 'type')\n            side = self.safe_string(order, 'side')\n            amount = self.safe_number(order, 'amount')\n            price = self.safe_number(order, 'price')\n            orderParams = self.safe_dict(order, 'params', {})\n            # Merge order-level params with top-level params\n            mergedParams = self.extend({}, params, orderParams)\n            # Get token_id from order params, order directly, or it will be resolved in buildOrder\n            tokenId = self.safe_string(orderParams, 'token_id') or self.safe_string(order, 'token_id')\n            if tokenId is not None:\n                mergedParams['token_id'] = tokenId\n            # Get clientOrderId from order params or order directly\n            clientOrderId = self.safe_string(orderParams, 'clientOrderId') or self.safe_string(order, 'clientOrderId')\n            if clientOrderId is not None:\n                mergedParams['clientOrderId'] = clientOrderId\n            # Get timeInForce from order params or order directly\n            timeInForce = self.safe_string(orderParams, 'timeInForce') or self.safe_string(order, 'timeInForce')\n            if timeInForce is not None:\n                mergedParams['timeInForce'] = timeInForce\n            # Build the order request payload using the shared buildOrder function\n            orderRequest = await self.build_order(symbol, type, side, amount, price, mergedParams)\n            # Store clientOrderId from request payload for response parsing\n            requestClientOrderId = self.safe_string(orderRequest, 'clientOrderId')\n            clientOrderIds.append(requestClientOrderId)\n            symbols.append(symbol)\n            orderRequests.append(orderRequest)\n        # Submit batch orders via POST /orders endpoint\n        response = await self.clob_private_post_orders(self.extend({'orders': orderRequests}, params))\n        # Response format: array of order responses, each with:\n        # {\n        #     \"success\": boolean,\n        #     \"errorMsg\": string(if error),\n        #     \"orderId\": string,\n        #     \"orderHashes\": string[](if order was marketable)\n        # }\n        result = []\n        for i in range(0, len(response)):\n            orderResponse = response[i]\n            success = self.safe_bool(orderResponse, 'success', True)\n            if not success:\n                errorMsg = self.safe_string(orderResponse, 'errorMsg', 'Unknown error')\n                raise ExchangeError(self.id + ' createOrders() failed for order ' + i + ': ' + errorMsg)\n            orderId = self.safe_string(orderResponse, 'orderID')\n            if orderId is None:\n                raise ExchangeError(self.id + ' createOrders() response missing orderID for order ' + i)\n            market = None\n            if symbols[i]:\n                market = self.market(symbols[i])\n            # Combine response with order details from orderRequests for parseOrder\n            orderData = self.extend({\n                'orderID': orderId,\n                'clientOrderId': clientOrderIds[i],\n                'order': orderRequests[i]['order'],  # Include the signed order for additional context\n                'order_type': orderRequests[i]['orderType'],  # Include orderType for parseOrder\n            }, orderResponse)\n            result.append(self.parse_order(orderData, market))\n        return result\n\n    async def create_market_order(self, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}):\n        \"\"\"\n        create a market order\n        :param str symbol: unified symbol of the market to create an order in\n        :param str side: 'buy' or 'sell'\n        :param float amount: how much you want to trade in units of the base currency\n        :param float [price]: ignored for market orders\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        # Use IOC by default for market orders(allows partial fills)\n        # Users can override with params.timeInForce = 'FOK' if they need Fill-Or-Kill behavior\n        return await self.create_order(symbol, 'market', side, amount, price, self.extend(params, {'timeInForce': 'IOC'}))\n\n    async def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order:\n        \"\"\"\n        cancels an open order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-order\n\n        :param str id: order id\n        :param str symbol: unified symbol of the market the order was made in\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Based on cancel() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL = \"/order\")\n        # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n        # See https://docs.polymarket.com/developers/CLOB/orders/cancel-order\n        response = await self.clob_private_delete_order(self.extend({'order_id': id}, params))\n        canceled = self.safe_list(response, 'canceled', [])\n        notCanceled = self.safe_dict(response, 'not_canceled', {})\n        # Check if order was successfully canceled\n        isCanceled = False\n        for i in range(0, len(canceled)):\n            if canceled[i] == id:\n                isCanceled = True\n                break\n        if isCanceled:\n            # Order was canceled, parse order from response data\n            market = self.market(symbol) if symbol else None\n            orderData = {\n                'id': id,\n                'status': 'canceled',\n                'info': response,\n            }\n            return self.parse_order(orderData, market)\n        else:\n            # Check if order is in not_canceled map\n            reason = self.safe_string(notCanceled, id)\n            if reason is not None:\n                # Order couldn't be canceled, raise error with reason\n                raise ExchangeError(self.id + ' cancelOrder() failed: ' + reason)\n            else:\n                # Order ID not found in response(shouldn't happen)\n                raise ExchangeError(self.id + ' cancelOrder() unexpected response format')\n\n    async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]:\n        \"\"\"\n        cancel multiple orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-order-batch\n\n        :param str[] ids: order ids\n        :param str symbol: unified symbol of the market the orders were made in\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: an array of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Based on cancel_orders() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ORDERS = \"/orders\")\n        # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n        # See https://docs.polymarket.com/developers/CLOB/orders/cancel-order-batch\n        response = await self.clob_private_delete_orders(self.extend({'order_ids': ids}, params))\n        canceled = self.safe_list(response, 'canceled', [])\n        notCanceled = self.safe_dict(response, 'not_canceled', {})\n        market = self.market(symbol) if symbol else None\n        orders: List[Order] = []\n        # Add canceled orders\n        for i in range(0, len(canceled)):\n            orderId = canceled[i]\n            orderData = {\n                'id': orderId,\n                'status': 'canceled',\n                'info': response,\n            }\n            orders.append(self.parse_order(orderData, market))\n        # Verify all requested orders are accounted for in the response\n        for i in range(0, len(ids)):\n            orderId = ids[i]\n            isInCanceled = False\n            for j in range(0, len(canceled)):\n                if canceled[j] == orderId:\n                    isInCanceled = True\n                    break\n            if not isInCanceled and not (orderId in notCanceled):\n                # Order ID not found in response(unexpected)\n                raise ExchangeError(self.id + ' cancelOrders() unexpected response format for order ' + orderId)\n        return orders\n\n    async def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]:\n        \"\"\"\n        cancel all open orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-all-orders\n\n        :param str [symbol]: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        response\n        if symbol is not None:\n            # Use cancel-market-orders endpoint when symbol is provided\n            # See https://docs.polymarket.com/developers/CLOB/orders/cancel-market-orders\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            # Get condition_id(market ID)\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            # Get asset_id from clobTokenIds\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            request: dict = {}\n            if conditionId is not None:\n                request['market'] = conditionId\n            if len(clobTokenIds) > 0:\n                request['asset_id'] = clobTokenIds[0]\n            # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n            response = await self.clob_private_delete_cancel_market_orders(self.extend(request, params))\n        else:\n            # Use cancel-all endpoint when symbol is None\n            # Based on cancel_all() from py-clob-client\n            # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ALL = \"/cancel-all\")\n            # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n            # See https://docs.polymarket.com/developers/CLOB/orders/cancel-all-orders\n            response = await self.clob_private_delete_cancel_all(params)\n        canceled = self.safe_list(response, 'canceled', [])\n        orderMarket = self.market(symbol) if symbol else None\n        orders: List[Order] = []\n        # Add canceled orders\n        for i in range(0, len(canceled)):\n            orderId = canceled[i]\n            orderData = {\n                'id': orderId,\n                'status': 'canceled',\n                'info': response,\n            }\n            orders.append(self.parse_order(orderData, orderMarket))\n        return orders\n\n    async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order:\n        \"\"\"\n        fetches information on an order made by the user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/get-order\n\n        :param str id: order id\n        :param str symbol: unified symbol of the market the order was made in\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Based on get_order() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_ORDER = \"/data/order/\")\n        response = await self.clob_private_get_order(self.extend({'order_id': id}, params))\n        market = self.market(symbol) if symbol else None\n        return self.parse_order(response, market)\n\n    async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:\n        \"\"\"\n        fetches information on multiple orders made by the user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/get-orders\n\n        :param str symbol: unified symbol of the market the orders were made in\n        :param int [since]: the earliest time in ms to fetch orders for\n        :param int [limit]: the maximum number of order structures to retrieve\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: filter orders by order id\n        :param str [params.market]: filter orders by market id\n        :param str [params.asset_id]: filter orders by asset id(alias token_id)\n        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.load_markets()\n        await self.ensure_api_credentials(params)\n        request = {}\n        if symbol is not None:\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            # Filter by condition_id(market) to get all orders for self market\n            # This is more appropriate than filtering by asset_id alone, market can have multiple outcomes\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            if conditionId is not None:\n                request['market'] = conditionId\n            # Also include asset_id for backward compatibility and more specific filtering\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # The Polymarket L2 getOpenOrders() endpoint filters by asset_id\n                request['asset_id'] = clobTokenIds[0]\n                # Keep backward compatibility for legacy token_id usage\n                request['token_id'] = clobTokenIds[0]\n        id = self.safe_string(params, 'id')\n        if id is not None:\n            request['id'] = id\n        marketId = self.safe_string(params, 'market')\n        if marketId is not None:\n            request['market'] = marketId\n        assetId = self.safe_string_2(params, 'asset_id', 'token_id')\n        if assetId is not None:\n            request['asset_id'] = assetId\n            request['token_id'] = assetId\n        initialCursor = self.safe_string(self.options, 'initialCursor')\n        endCursor = self.safe_string(self.options, 'endCursor')\n        nextCursor = initialCursor\n        ordersResponse: List[Any] = []\n        while(True):\n            response = await self.clob_private_get_orders(self.extend(request, {'next_cursor': nextCursor}, params))\n            data = self.safe_list(response, 'data', [])\n            ordersResponse = self.array_concat(ordersResponse, data)\n            if limit is not None and len(ordersResponse) >= limit:\n                break\n            nextCursor = self.safe_string(response, 'next_cursor')\n            if nextCursor is None or nextCursor == endCursor:\n                break\n        orderMarket = self.market(symbol) if symbol else None\n        return self.parse_orders(ordersResponse, orderMarket, since, limit)\n\n    async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:\n        \"\"\"\n        fetch all unfilled currently open orders\n        :param str symbol: unified symbol of the market to fetch open orders for\n        :param int [since]: the earliest time in ms to fetch open orders for\n        :param int [limit]: the maximum number of open order structures to retrieve\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        # The Polymarket getOpenOrders() endpoint already returns open orders\n        return await self.fetch_orders(symbol, since, limit, params)\n\n    def parse_order(self, order: dict, market: Market = None) -> Order:\n        \"\"\"\n        parses an order from the exchange response format\n        :param dict order: order response from the exchange\n        :param dict [market]: market structure\n        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        # Handle createOrder/createOrders response format:\n        # {\n        #   \"success\": boolean,\n        #   \"errorMsg\": string(if error),\n        #   \"orderID\": string,\n        #   \"orderHashes\": string[](if order was marketable)\n        # }\n        # Or fetchOrder response format(OpenOrder interface):\n        # {\n        #   id: string\n        #   status: string\n        #   owner: string\n        #   maker_address: string\n        #   market: string\n        #   asset_id: string\n        #   side: string\n        #   original_size: string\n        #   size_matched: string\n        #   price: string\n        #   associate_trades: string[]\n        #   outcome: string\n        #   created_at: number  # seconds\n        #   expiration: string\n        #   order_type: string\n        # }\n        id = self.safe_string(order, 'id')\n        # Handle createOrder response format(has orderID instead of id)\n        if id is None:\n            id = self.safe_string(order, 'orderID')\n        marketId = self.safe_string(order, 'market')\n        assetId = self.safe_string(order, 'asset_id')\n        if market is None and marketId is not None:\n            market = self.safe_market(marketId, None)\n        symbol = None\n        if market is not None and market['symbol'] is not None:\n            symbol = market['symbol']\n        elif assetId is not None:\n            symbol = assetId\n        # Handle createOrder response - get side from order object if available\n        sideStr = self.safe_string_lower(order, 'side')\n        # If side is not in order, try to get it from the order object passed in createOrder\n        if sideStr is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                sideStr = self.safe_string_lower(orderObj, 'side')\n        side = sideStr if (sideStr == 'buy' or sideStr == 'sell') else None\n        orderType = self.safe_string(order, 'order_type')\n        # Handle createOrder response - get orderType from order object if available\n        if orderType is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                orderType = self.safe_string(orderObj, 'orderType')\n            # Also check at top level(from requestPayload)\n            if orderType is None:\n                orderType = self.safe_string(order, 'orderType')\n        # Normalize orderType to lowercase for consistent parsing\n        if orderType is not None:\n            orderType = orderType.lower()\n        # Amounts\n        amount = self.safe_number(order, 'original_size')\n        # Handle createOrder response - get amount from order object if available\n        if amount is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                amount = self.safe_number(orderObj, 'size')\n        filled = self.safe_number(order, 'size_matched')\n        remaining = self.safe_number(order, 'remaining_size')\n        if remaining is None and amount is not None and filled is not None:\n            remaining = amount - filled\n        # Price\n        price = self.safe_number(order, 'price')\n        # Handle createOrder response - get price from order object if available\n        if price is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                price = self.safe_number(orderObj, 'price')\n        # Status\n        statusStr = self.safe_string(order, 'status', '')\n        status = self.parse_order_status(statusStr)\n        # Timestamps(created_at is seconds)\n        createdAt = self.safe_integer(order, 'created_at')\n        timestamp = createdAt * 1000 if (createdAt is not None) else None\n        # Get clientOrderId from order or from the order object\n        clientOrderId = self.safe_string(order, 'clientOrderId')\n        if clientOrderId is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                clientOrderId = self.safe_string(orderObj, 'clientOrderId')\n        # No explicit updated_at in interface; leave lastTradeTimestamp None\n        return self.safe_order({\n            'id': id,\n            'clientOrderId': clientOrderId,\n            'info': order,\n            'timestamp': timestamp,\n            'datetime': self.iso8601(timestamp) if timestamp else None,\n            'lastTradeTimestamp': None,\n            'status': status,\n            'symbol': symbol,\n            'type': self.parse_order_type(orderType),\n            'timeInForce': self.parse_time_in_force(orderType),\n            'side': side,\n            'price': price,\n            'amount': amount,\n            'cost': None,\n            'average': None,\n            'filled': filled,\n            'remaining': remaining,\n            'fee': None,\n        }, market)\n\n    def parse_order_status(self, status: Str) -> Str:\n        \"\"\"\n        parse the status of an order\n        :param str status: order status from exchange\n        :returns str: a unified order status\n        \"\"\"\n        if status is None or status == '':\n            return 'open'  # Default to 'open' if no status is provided\n        statuses: dict = {\n            # https://docs.polymarket.com/developers/CLOB/orders/create-order#status\n            'matched': 'closed',   # order placed and matched with an existing resting order\n            'live': 'open',         # order placed and resting on the book\n            'delayed': 'open',      # order marketable, but subject to matching delay\n            'unmatched': 'open',    # order marketable, but failure delaying, placement successful\n            'canceled': 'canceled',  # CCXT unified status for canceled orders\n        }\n        normalizedStatus = status.lower()\n        return self.safe_string(statuses, normalizedStatus, normalizedStatus)\n\n    def parse_order_type(self, type: Str) -> Str:\n        types: dict = {\n            'fok': 'market',  # Fill-Or-Kill: market order\n            'fak': 'market',  # Fill-And-Kill: market order\n            'ioc': 'market',  # Immediate-Or-Cancel: market order\n            'gtc': 'limit',  # Good-Til-Cancelled: limit order\n            'gtd': 'limit',  # Good-Til-Date: limit order\n        }\n        return self.safe_string(types, type, 'limit')\n\n    def parse_time_in_force(self, timeInForce: Str) -> Str:\n        if timeInForce is None:\n            return None\n        timeInForces: dict = {\n            'fok': 'FOK',  # Fill-Or-Kill\n            'fak': 'FAK',  # Fill-And-Kill\n            'ioc': 'IOC',  # Immediate-Or-Cancel\n            'gtc': 'GTC',  # Good-Til-Cancelled\n            'gtd': 'GTD',  # Good-Til-Date\n        }\n        normalized = timeInForce.lower()\n        mapped = self.safe_string(timeInForces, normalized)\n        return mapped is not mapped if None else timeInForce.upper()\n\n    async def fetch_time(self, params={}) -> Int:\n        \"\"\"\n        fetches the current integer timestamp in milliseconds from the exchange server\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L178\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns int: the current integer timestamp in milliseconds from the exchange server\n        \"\"\"\n        # Based on get_server_time() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L178\n        response = await self.clob_public_get_time(params)\n        # Response format: timestamp in seconds(Unix timestamp)\n        # Convert to milliseconds for CCXT standard\n        timestamp = self.safe_integer(response, 'timestamp')\n        if timestamp is not None:\n            return timestamp * 1000  # Convert seconds to milliseconds\n        # Fallback: if response is just a number\n        if isinstance(response, numbers.Real):\n            return response * 1000\n        # Fallback: use current time if server time not available\n        return self.milliseconds()\n\n    async def fetch_status(self, params={}):\n        \"\"\"\n        the latest known information on the availability of the exchange API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: a `status structure <https://docs.ccxt.com/#/?id=exchange-status-structure>`\n        \"\"\"\n        # Based on get_ok() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n        try:\n            await self.clob_public_get_ok(params)\n            return {\n                'status': 'ok',\n                'updated': None,\n                'eta': None,\n                'url': None,\n            }\n        except Exception as e:\n            return {\n                'status': 'error',\n                'updated': None,\n                'eta': None,\n                'url': None,\n            }\n\n    async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface:\n        \"\"\"\n        fetches the trading fee for a market\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param str symbol: unified symbol of the market to fetch the fee for\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required if not in market info)\n        :returns dict: a `fee structure <https://docs.ccxt.com/#/?id=fee-structure>`\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchTradingFee() requires a token_id parameter for market ' + symbol)\n        # Based on get_fee_rate() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        response = await self.clob_public_get_fee_rate(self.extend({'token_id': tokenId}, params))\n        # Response format: {\"fee_rate\": \"0.02\"} or {\"fee_rate_bps\": 200}(basis points)\n        feeRate = self.safe_string(response, 'fee_rate')\n        feeRateBps = self.safe_integer(response, 'fee_rate_bps')\n        maker: Num = None\n        taker: Num = None\n        if feeRate is not None:\n            fee = self.parse_number(feeRate)\n            maker = fee\n            taker = fee\n        elif feeRateBps is not None:\n            # Convert basis points to percentage(200 bps = 2% = 0.02)\n            fee = self.parse_number(feeRateBps) / 10000\n            maker = fee\n            taker = fee\n        else:\n            # Default fee from describe() if not available\n            maker = self.safe_number(self.fees['trading'], 'maker')\n            taker = self.safe_number(self.fees['trading'], 'taker')\n        # Ensure we have valid numbers(fallback to default if None)\n        makerFee: Num = maker is not maker if None else self.parse_number('0.02')\n        takerFee: Num = taker is not taker if None else self.parse_number('0.02')\n        result: TradingFeeInterface = {\n            'info': response,\n            'symbol': symbol,\n            'maker': makerFee,\n            'taker': takerFee,\n            'percentage': True,\n            'tierBased': False,\n        }\n        return result\n\n    async def fetch_open_interest(self, symbol: str, params={}):\n        \"\"\"\n        retrieves the open interest of a market\n\n        https://docs.polymarket.com/api-reference/misc/get-open-interest\n\n        :param str symbol: unified CCXT market symbol\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure:\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n        # API expects market of condition IDs\n        request: dict = {\n            'market': [conditionId],\n        }\n        response = await self.data_public_get_open_interest(self.extend(request, params))\n        return self.parse_open_interest(response, market)\n\n    def parse_open_interest(self, interest: dict, market: Market = None):\n        \"\"\"\n        parses open interest data from the exchange response format\n        :param dict interest: open interest data from the exchange\n        :param dict [market]: the market self open interest is for\n        :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure:\n        \"\"\"\n        # Polymarket Data API /oi response format\n        # Response is an array of objects with market(condition ID) and value\n        # Example response structure:\n        # [\n        #   {\n        #     \"market\": \"0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917\",\n        #     \"value\": 123\n        #   }\n        # ]\n        timestamp = self.milliseconds()\n        # Handle array response\n        openInterestData: dict = {}\n        if isinstance(interest, list):\n            # For single symbol query, get the first item\n            if len(interest) > 0:\n                openInterestData = interest[0]\n        elif isinstance(interest, dict) and interest != None:\n            # Fallback: handle object response if API changes\n            openInterestData = interest\n        # Extract open interest value from the response\n        # API returns \"value\" field which represents the open interest value\n        openInterestValue = self.safe_number(openInterestData, 'value', 0)\n        # For Polymarket, value is typically in USDC, so we use it amount and value\n        # If we need to distinguish, we could parse additional fields if available\n        return self.safe_open_interest({\n            'symbol': market['symbol'] if market else None,\n            'openInterestAmount': openInterestValue,  # Using value since API only provides value\n            'openInterestValue': openInterestValue,\n            'timestamp': timestamp,\n            'datetime': self.iso8601(timestamp),\n            'info': interest,\n        }, market)\n\n    async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        fetch all trades made by the user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param str symbol: unified symbol of the market to fetch trades for\n        :param int [since]: the earliest time in ms to fetch trades for\n        :param int [limit]: the maximum number of trades structures to retrieve\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: filter trades by market(condition_id)\n        :param str [params.asset_id]: filter trades by asset ID\n        :param str [params.id]: filter by trade id\n        :param str [params.maker_address]: filter by maker address\n        :param str [params.before]: pagination cursor(see API docs)\n        :param str [params.after]: pagination cursor(see API docs)\n        :param str [params.next_cursor]: pagination cursor(default: \"MA==\")\n        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        request: dict = {}\n        market = None\n        if symbol is not None:\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            # Filter by condition_id(market) to get all trades for self market\n            # Don't automatically add asset_id filter would restrict to only one outcome\n            conditionId = self.safe_string(marketInfo, 'condition_id', self.safe_string(market, 'id'))\n            if conditionId is not None:\n                request['market'] = conditionId\n        # Backward compatibility: token_id alias to asset_id\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is not None:\n            request['asset_id'] = tokenId\n        marketId = self.safe_string(params, 'market')\n        if marketId is not None:\n            request['market'] = marketId\n        assetId = self.safe_string_2(params, 'asset_id', 'assetId')\n        if assetId is not None:\n            request['asset_id'] = assetId\n        id = self.safe_string(params, 'id')\n        if id is not None:\n            request['id'] = id\n        makerAddress = self.safe_string_2(params, 'maker_address', 'makerAddress')\n        if makerAddress is not None:\n            request['maker_address'] = makerAddress\n        before = self.safe_string(params, 'before')\n        if before is not None:\n            request['before'] = before\n        after = self.safe_string(params, 'after')\n        if after is not None:\n            request['after'] = after\n        if since is not None:\n            # Map ccxt since to Polymarket's \"after\" cursor using seconds\n            request['after'] = self.number_to_string(int(math.floor(since / 1000)))\n        if limit is not None:\n            request['limit'] = limit\n        results: List[Any] = []\n        initialCursor = self.safe_string(self.options, 'initialCursor')\n        endCursor = self.safe_string(self.options, 'endCursor')\n        next_cursor = initialCursor\n        while(next_cursor != endCursor):\n            response = await self.clob_private_get_trades(self.extend(request, {'next_cursor': next_cursor}, params))\n            next_cursor = self.safe_string(response, 'next_cursor', endCursor)\n            data = self.safe_list(response, 'data', []) or []\n            results = self.array_concat(results, data)\n            if limit is not None and len(results) >= limit:\n                break\n        return self.parse_trades(results, market, since, limit)\n\n    async def fetch_user_trades(self, user: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        fetch trades for a specific user\n\n        https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n\n        :param str user: user address(0x-prefixed, 40 hex chars)\n        :param str [symbol]: unified symbol of the market to fetch trades for\n        :param int [since]: timestamp in ms of the earliest trade to fetch\n        :param int [limit]: the maximum amount of trades to fetch(default: 100, max: 10000)\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.offset]: offset for pagination(default: 0, max: 10000)\n        :param boolean [params.takerOnly]: if True, returns only trades where the user is the taker(default: True)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :param str[] [params.market]: comma-separated list of condition IDs(mutually exclusive with symbol)\n        :param int[] [params.eventId]: comma-separated list of event IDs(mutually exclusive with market)\n        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`\n        \"\"\"\n        await self.load_markets()\n        request: dict = {\n            'user': user,\n        }\n        market = None\n        if symbol is not None:\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            request['market'] = [conditionId]\n        marketParam = self.safe_value(params, 'market')\n        if marketParam is not None:\n            # Convert to array if it's a string or single value\n            if isinstance(marketParam, list):\n                request['market'] = marketParam\n            else:\n                request['market'] = [marketParam]\n        eventId = self.safe_value(params, 'eventId')\n        if eventId is not None:\n            if isinstance(eventId, list):\n                request['eventId'] = eventId\n            else:\n                request['eventId'] = [eventId]\n        if limit is not None:\n            request['limit'] = min(limit, 10000)  # Cap at max 10000\n        offset = self.safe_integer(params, 'offset')\n        if offset is not None:\n            request['offset'] = offset\n        takerOnly = self.safe_bool(params, 'takerOnly', True)\n        request['takerOnly'] = takerOnly\n        side = self.safe_string_upper(params, 'side')\n        if side is not None:\n            request['side'] = side\n        response = await self.data_public_get_trades(self.extend(request, self.omit(params, ['market', 'eventId', 'offset', 'takerOnly', 'side'])))\n        tradesData = []\n        if isinstance(response, list):\n            tradesData = response\n        else:\n            dataList = self.safe_list(response, 'data', [])\n            if dataList is not None:\n                tradesData = dataList\n        return self.parse_trades(tradesData, market, since, limit)\n\n    async def fetch_balance(self, params={}):\n        \"\"\"\n        fetches balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/clients/methods-l2#getbalanceallowance\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_type]: asset type: 'COLLATERAL'(default) or 'CONDITIONAL'\n        :param str [params.token_id]: token ID, default: from options.defaultTokenId)\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Default asset_type to COLLATERAL if not provided\n        assetType = self.safe_string(params, 'asset_type', 'COLLATERAL')\n        params['asset_type'] = assetType\n        # Use signature_type from params or fall back to options\n        signatureType = self.get_signature_type(params)\n        request: dict = {\n            'asset_type': assetType,\n        }\n        if signatureType is not None:\n            request['signature_type'] = signatureType\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            defaultTokenId = self.safe_string(self.options, 'defaultTokenId')\n            if defaultTokenId is not None:\n                request['token_id'] = defaultTokenId\n        else:\n            request['token_id'] = tokenId\n        # Fetch balance and allowance from CLOB endpoint\n        clobResponse = await self.clob_private_get_balance_allowance(request)\n        #\n        #     {\n        #         \"balance\": \"1000000\",\n        #         \"allowance\": \"0\"\n        #     }\n        #\n        balance = self.safe_string(clobResponse, 'balance')\n        allowance = self.safe_string(clobResponse, 'allowance')\n        collateral = self.safe_string(self.options, 'defaultCollateral', 'USDC')\n        # Convert CLOB balance and allowance(6 decimals) to standard units\n        collateralTotalValue = None\n        collateralUsedValue = None\n        collateralFreeValue = None\n        if balance is not None:\n            parsedBalance = self.parse_number(balance)\n            if parsedBalance is not None:\n                collateralTotalValue = parsedBalance / 1000000\n        if allowance is not None:\n            parsedAllowance = self.parse_number(allowance)\n            if parsedAllowance is not None:\n                collateralUsedValue = parsedAllowance / 1000000\n        # Calculate free balance: total - used(allowance)\n        if collateralTotalValue is not None and collateralUsedValue is not None:\n            collateralFreeValue = collateralTotalValue - collateralUsedValue\n        elif collateralTotalValue is not None:\n            collateralFreeValue = collateralTotalValue\n        result: dict = {\n            'info': clobResponse,\n        }\n        if collateralTotalValue is not None:\n            account = self.account()\n            account['total'] = collateralTotalValue\n            if collateralFreeValue is not None:\n                account['free'] = collateralFreeValue\n            if collateralUsedValue is not None:\n                account['used'] = collateralUsedValue\n            result[collateral] = account\n        return self.safe_balance(result)\n\n    async def get_notifications(self, params={}):\n        \"\"\"\n        fetches notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Use signature_type from params or fall back to options\n        signatureType = self.get_signature_type(params)\n        request: dict = {}\n        if signatureType is not None:\n            request['signature_type'] = signatureType\n        # Based on get_notifications() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        response = await self.clob_private_get_notifications(self.extend(request, params))\n        return response\n\n    async def drop_notifications(self, params={}):\n        \"\"\"\n        drops notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.notification_id]: specific notification ID to drop(optional)\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Use signature_type from params or fall back to options\n        signatureType = self.get_signature_type(params)\n        request: dict = {}\n        if signatureType is not None:\n            request['signature_type'] = signatureType\n        # Based on drop_notifications() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        response = await self.clob_private_delete_notifications(self.extend(request, params))\n        return response\n\n    async def get_balance_allowance(self, params={}):\n        \"\"\"\n        fetches balance and allowance for the authenticated user(alias for fetchBalance)\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/clients/methods-l2#getbalanceallowance\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_type]: asset type: 'COLLATERAL'(default) or 'CONDITIONAL'\n        :param str [params.token_id]: token ID, default: from options.defaultTokenId)\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Alias for fetchBalance, but returns raw response\n        # Use signature_type from params or fall back to options\n        if self.safe_integer(params, 'signature_type') is None:\n            signatureType = self.get_signature_type(params)\n            if signatureType is not None:\n                params['signature_type'] = signatureType\n        # Default asset_type to COLLATERAL if not provided(for USDC balance)\n        assetType = self.safe_string(params, 'asset_type', 'COLLATERAL')\n        params['asset_type'] = assetType\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            defaultTokenId = self.safe_string(self.options, 'defaultTokenId')\n            if defaultTokenId is not None:\n                params['token_id'] = defaultTokenId\n        else:\n            params['token_id'] = tokenId\n        return await self.clob_private_get_balance_allowance(params)\n\n    async def update_balance_allowance(self, params={}):\n        \"\"\"\n        updates balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        # Based on update_balance_allowance() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        # Use signature_type from params or fall back to options\n        if self.safe_integer(params, 'signature_type') is None:\n            signatureType = self.get_signature_type(params)\n            if signatureType is not None:\n                params['signature_type'] = signatureType\n        response = await self.clob_private_put_balance_allowance(params)\n        return response\n\n    async def is_order_scoring(self, params={}):\n        \"\"\"\n        checks if an order is currently scoring\n\n        https://docs.polymarket.com/developers/CLOB/orders/check-scoring#check-order-reward-scoring\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID to check(required)\n        :returns dict: response from the exchange indicating if order is scoring\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        orderId = self.safe_string(params, 'order_id')\n        if orderId is None:\n            raise ArgumentsRequired(self.id + ' isOrderScoring() requires an order_id parameter')\n        response = await self.clob_private_get_is_order_scoring(params)\n        # Response: {scoring: boolean}\n        return response\n\n    async def are_orders_scoring(self, params={}):\n        \"\"\"\n        checks if multiple orders are currently scoring\n\n        https://docs.polymarket.com/developers/CLOB/orders/check-scoring#check-order-reward-scoring\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.order_ids]: array of order IDs to check(required)\n        :returns dict: response from the exchange indicating which orders are scoring\n        \"\"\"\n        await self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        await self.ensure_api_credentials(params)\n        orderIds = self.safe_value_2(params, 'order_ids', 'orderIds')\n        if orderIds is None or not isinstance(orderIds, list):\n            raise ArgumentsRequired(self.id + ' areOrdersScoring() requires an order_ids parameter(array of order IDs)')\n        response = await self.clob_private_post_are_orders_scoring(self.extend({'orderIds': orderIds}, params))\n        # Response: {orderId: boolean, ...}\n        return response\n\n    async def clob_public_get_markets(self, params={}):\n        \"\"\"\n        fetches markets from CLOB API(matches clob-client getMarkets())\n\n        https://github.com/Polymarket/clob-client/blob/main/src/client.ts\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.next_cursor]: pagination cursor(default: options.initialCursor)\n        :returns dict: response from the exchange\n        \"\"\"\n        # Pass api ['clob', 'public'] to match the expected format\n        # The api parameter should be an array [api_type, access_level] for exchanges with multiple API types\n        return await self.request('markets', ['clob', 'public'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def gamma_public_get_markets(self, params={}):\n        \"\"\"\n        fetches markets from Gamma API\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # Pass api ['gamma', 'public'] to match the expected format\n        # The api parameter should be an array [api_type, access_level] for exchanges with multiple API types\n        return await self.request('markets', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    async def gamma_public_get_markets_id(self, params={}):\n        \"\"\"\n        fetches a specific market by ID from Gamma API\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetMarketsId() requires an id parameter')\n        path = 'markets/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def gamma_public_get_markets_id_tags(self, params={}):\n        \"\"\"\n        fetches tags for a specific market by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the market ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetMarketsIdTags() requires an id parameter')\n        path = 'markets/' + self.encode_uri_component(id) + '/tags'\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def gamma_public_get_markets_slug_slug(self, params={}):\n        \"\"\"\n        fetches a specific market by slug from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.slug]: the market slug(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        slug = self.safe_string(params, 'slug')\n        if slug is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetMarketsSlugSlug() requires a slug parameter')\n        path = 'markets/slug/' + self.encode_uri_component(slug)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'slug'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def gamma_public_get_events(self, params={}):\n        \"\"\"\n        fetches events from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :param str [params.category]: filter by category\n        :param str [params.slug]: filter by slug\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('events', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    async def gamma_public_get_events_id(self, params={}):\n        \"\"\"\n        fetches a specific event by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the event ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetEventsId() requires an id parameter')\n        path = 'events/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def gamma_public_get_series(self, params={}):\n        \"\"\"\n        fetches series from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :param str [params.category]: filter by category\n        :param str [params.slug]: filter by slug\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('series', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    async def gamma_public_get_series_id(self, params={}):\n        \"\"\"\n        fetches a specific series by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the series ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetSeriesId() requires an id parameter')\n        path = 'series/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def gamma_public_get_search(self, params={}):\n        \"\"\"\n        performs a full-text search across events, tags, and user profiles from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.q]: search query(required)\n        :param str [params.type]: filter by type: 'event', 'tag', 'user', etc.\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        q = self.safe_string(params, 'q')\n        if q is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetSearch() requires a q(query) parameter')\n        return await self.request('search', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    async def gamma_public_get_comments(self, params={}):\n        \"\"\"\n        fetches comments from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.event_id]: filter by event ID\n        :param str [params.series_id]: filter by series ID\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('comments', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    async def gamma_public_get_comments_id(self, params={}):\n        \"\"\"\n        fetches a specific comment by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the comment ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetCommentsId() requires an id parameter')\n        path = 'comments/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def gamma_public_get_sports(self, params={}):\n        \"\"\"\n        fetches sports data from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.league]: filter by league\n        :param str [params.team]: filter by team\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('sports', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    async def gamma_public_get_sports_id(self, params={}):\n        \"\"\"\n        fetches a specific sport/team by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the sport/team ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetSportsId() requires an id parameter')\n        path = 'sports/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return await self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    async def data_public_get_positions(self, params={}):\n        \"\"\"\n        fetches current positions for a user from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :param str[] [params.market]: comma-separated list of condition IDs(mutually exclusive with eventId)\n        :param int[] [params.eventId]: comma-separated list of event IDs(mutually exclusive with market)\n        :param number [params.sizeThreshold]: minimum size threshold(default: 1)\n        :param boolean [params.redeemable]: filter by redeemable positions(default: False)\n        :param boolean [params.mergeable]: filter by mergeable positions(default: False)\n        :param int [params.limit]: maximum number of results(default: 100, max: 500)\n        :param int [params.offset]: offset for pagination(default: 0, max: 10000)\n        :param str [params.sortBy]: sort field: CURRENT, INITIAL, TOKENS, CASHPNL, PERCENTPNL, TITLE, RESOLVING, PRICE, AVGPRICE(default: TOKENS)\n        :param str [params.sortDirection]: sort direction: ASC, DESC(default: DESC)\n        :param str [params.title]: filter by title(max length: 100)\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetPositions() requires a user parameter')\n        return await self.request('positions', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_trades(self, params={}):\n        \"\"\"\n        fetches trades for a user or markets from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(optional, filter by user)\n        :param str[] [params.market]: comma-separated list of condition IDs(optional, filter by markets)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('trades', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_activity(self, params={}):\n        \"\"\"\n        fetches user activity from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-user-activity\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetActivity() requires a user parameter')\n        return await self.request('activity', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_holders(self, params={}):\n        \"\"\"\n        fetches top holders for markets from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-top-holders-for-markets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.market]: comma-separated list of condition IDs(required)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_string(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetHolders() requires a market parameter')\n        return await self.request('holders', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_total_value(self, params={}):\n        \"\"\"\n        fetches total value of a user's positions from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-total-value-of-a-users-positions\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetTotalValue() requires a user parameter')\n        return await self.request('value', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_closed_positions(self, params={}):\n        \"\"\"\n        fetches closed positions for a user from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-closed-positions-for-a-user\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :param str[] [params.market]: comma-separated list of condition IDs(mutually exclusive with eventId)\n        :param int[] [params.eventId]: comma-separated list of event IDs(mutually exclusive with market)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :param str [params.sortBy]: sort field\n        :param str [params.sortDirection]: sort direction: ASC, DESC\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetClosedPositions() requires a user parameter')\n        return await self.request('closed-positions', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_traded(self, params={}):\n        \"\"\"\n        fetches total markets a user has traded from Data-API\n\n        https://docs.polymarket.com/api-reference/misc/get-total-markets-a-user-has-traded\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetTraded() requires a user parameter')\n        return await self.request('traded', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def data_public_get_open_interest(self, params={}):\n        \"\"\"\n        fetches open interest from Data-API\n\n        https://docs.polymarket.com/api-reference/misc/get-open-interest\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.market]: array of condition IDs(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_value(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetOpenInterest() requires a market parameter')\n        # Convert market to array if it's a single string\n        marketArray: List[str] = []\n        if isinstance(market, list):\n            marketArray = market\n        elif isinstance(market, str):\n            marketArray = [market]\n        else:\n            raise ArgumentsRequired(self.id + ' dataPublicGetOpenInterest() requires market to be a string or array of condition IDs')\n        # API expects market in query params\n        requestParams = self.extend({'market': marketArray}, self.omit(params, 'market'))\n        return await self.request('oi', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, requestParams))\n\n    async def data_public_get_live_volume(self, params={}):\n        \"\"\"\n        fetches live volume for an event from Data-API\n\n        https://docs.polymarket.com/api-reference/misc/get-live-volume-for-an-event\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.eventId]: event ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        eventId = self.safe_integer(params, 'eventId')\n        if eventId is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetLiveVolume() requires an eventId parameter')\n        return await self.request('live-volume', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    async def bridge_public_get_supported_assets(self, params={}):\n        \"\"\"\n        fetches supported assets for bridging from Bridge API\n\n        https://docs.polymarket.com/developers/misc-endpoints/bridge-supported-assets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('supported-assets', ['bridge', 'public'], 'GET', self.extend({'api_type': 'bridge'}, params))\n\n    async def bridge_public_post_deposit(self, params={}):\n        \"\"\"\n        creates deposit addresses for bridging assets to Polymarket\n\n        https://docs.polymarket.com/developers/misc-endpoints/bridge-deposit\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.address]: Polymarket wallet address(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        address = self.safe_string(params, 'address')\n        if address is None:\n            raise ArgumentsRequired(self.id + ' bridgePublicPostDeposit() requires an address parameter')\n        body = self.json({'address': address})\n        remainingParams = self.extend({'api_type': 'bridge'}, self.omit(params, 'address'))\n        return await self.request('deposit', ['bridge', 'public'], 'POST', remainingParams, None, body)\n\n    async def create_deposit_address(self, code: str, params={}):\n        \"\"\"\n        create a deposit address for bridging assets to Polymarket\n\n        https://docs.polymarket.com/developers/misc-endpoints/bridge-deposit\n\n        :param str code: unified currency code\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.address]: Polymarket wallet address(required if not set in options)\n        :returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`\n        \"\"\"\n        # Get address from params or use default from options\n        address = self.safe_string(params, 'address')\n        if address is None:\n            # Try to get from options or raise error\n            address = self.safe_string(self.options, 'address')\n            if address is None:\n                raise ArgumentsRequired(self.id + ' createDepositAddress() requires an address parameter or address in options')\n        response = await self.bridge_public_post_deposit(self.extend({'address': address}, params))\n        # Response format: {address: \"...\", depositAddresses: [{chainId, chainName, tokenAddress, tokenSymbol, depositAddress}, ...]}\n        depositAddresses = self.safe_list(response, 'depositAddresses', [])\n        # Find the deposit address for the requested currency code\n        # For Polymarket, all deposits are converted to USDC.e, but we can filter by tokenSymbol\n        currency = self.currency(code)\n        depositAddress = None\n        for i in range(0, len(depositAddresses)):\n            addr = depositAddresses[i]\n            tokenSymbol = self.safe_string(addr, 'tokenSymbol')\n            if tokenSymbol and tokenSymbol.upper() == currency['code'].upper():\n                depositAddress = self.safe_string(addr, 'depositAddress')\n                break\n        # If not found, return the first deposit address(default to USDC)\n        if depositAddress is None and len(depositAddresses) > 0:\n            depositAddress = self.safe_string(depositAddresses[0], 'depositAddress')\n        return {\n            'currency': code,\n            'address': depositAddress,\n            'tag': None,\n            'info': response,\n        }\n\n    async def clob_public_get_orderbook_token_id(self, params={}):\n        \"\"\"\n        fetches orderbook for a specific token ID from CLOB API\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetOrderbookTokenId() requires a token_id parameter')\n        # Note: CLOB API uses /book endpoint with token_id parameter, not /orderbook/{token_id}\n        # See https://docs.polymarket.com/developers/CLOB/prices-books/get-book\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('book', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_post_books(self, params={}):\n        \"\"\"\n        fetches order books for multiple token IDs from CLOB API\n\n        https://docs.polymarket.com/api-reference/orderbook/get-multiple-order-books-summaries-by-request\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param Array [params.requests]: array of {token_id, limit?} objects(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        requests = self.safe_value(params, 'requests')\n        if requests is None or not isinstance(requests, list) or len(requests) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicPostBooks() requires a requests parameter(array of {token_id, limit?} objects)')\n        # Note: REST API endpoint format: POST /books with JSON body\n        # See https://docs.polymarket.com/api-reference/orderbook/get-multiple-order-books-summaries-by-request\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\", limit: 10}, ...]\n        # Response format: array of order book objects, each with asset_id, bids, asks, etc.\n        body = self.json(requests)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'requests'))\n        return await self.request('books', ['clob', 'public'], 'POST', remainingParams, None, body)\n\n    async def clob_public_get_market_trades_events(self, params={}):\n        \"\"\"\n        fetches market trade events for a specific condition ID from CLOB API\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getmarkettradesevents\n        https://docs.polymarket.com/developers/CLOB/trades/trades-data-api\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.condition_id]: the condition ID(market ID) for the market\n        :param int [params.limit]: the maximum number of trades to fetch(default: 100, max: 500)\n        :param int [params.offset]: number of trades to skip before starting to return results(default: 0)\n        :param boolean [params.takerOnly]: if True, returns only trades where the user is the taker(default: True)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :returns dict: response from the exchange\n        \"\"\"\n        conditionId = self.safe_string(params, 'condition_id')\n        if conditionId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetMarketTradesEvents() requires a condition_id parameter')\n        # Note: CLOB REST API endpoint format: /trades?market={condition_id}\n        # See https://docs.polymarket.com/developers/CLOB/trades/trades-data-api\n        # The client SDK method getMarketTradesEvents() uses a different endpoint, but the REST API uses /trades\n        request: dict = {\n            'market': conditionId,\n        }\n        remainingParams = self.omit(params, 'condition_id')\n        # Add optional parameters\n        limit = self.safe_integer(remainingParams, 'limit')\n        if limit is not None:\n            request['limit'] = limit\n        offset = self.safe_integer(remainingParams, 'offset')\n        if offset is not None:\n            request['offset'] = offset\n        takerOnly = self.safe_bool(remainingParams, 'takerOnly')\n        if takerOnly is not None:\n            request['takerOnly'] = takerOnly\n        side = self.safe_string(remainingParams, 'side')\n        if side is not None:\n            request['side'] = side\n        # Add any other remaining params\n        finalParams = self.extend({'api_type': 'clob'}, self.extend(request, self.omit(remainingParams, ['limit', 'offset', 'takerOnly', 'side'])))\n        return await self.request('trades', ['clob', 'public'], 'GET', finalParams)\n\n    async def clob_public_get_prices_history(self, params={}):\n        \"\"\"\n        fetches historical price data for a token from CLOB API\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getpriceshistory\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: the token ID(market parameter)\n        :param str [params.interval]: the time interval: \"max\", \"1w\", \"1d\", \"6h\", \"1h\"\n        :param int [params.startTs]: timestamp in seconds of the earliest candle to fetch\n        :param int [params.endTs]: timestamp in seconds of the latest candle to fetch\n        :param number [params.fidelity]: data fidelity/quality\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_string(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetPricesHistory() requires a market(token_id) parameter')\n        # Note: REST API endpoint format: /prices-history\n        # See https://docs.polymarket.com/developers/CLOB/timeseries\n        # Required: market\n        # Time component(mutually exclusive): either(startTs and endTs) OR interval\n        # Optional: fidelity\n        # Response format: {\"history\": [{\"t\": timestamp, \"p\": price}, ...]}\n        request: dict = {\n            'market': market,\n        }\n        # Add time component - either startTs/endTs OR interval(mutually exclusive)\n        startTs = self.safe_integer(params, 'startTs')\n        endTs = self.safe_integer(params, 'endTs')\n        interval = self.safe_string(params, 'interval')\n        if startTs is not None or endTs is not None:\n            # Use startTs/endTs when provided\n            if startTs is not None:\n                request['startTs'] = startTs\n            if endTs is not None:\n                request['endTs'] = endTs\n        elif interval is not None:\n            # Use interval when startTs/endTs are not provided\n            request['interval'] = interval\n        # Add optional fidelity parameter\n        fidelity = self.safe_number(params, 'fidelity')\n        if fidelity is not None:\n            finalFidelity = fidelity\n            # Polymarket enforces minimum fidelity per interval(e.g. interval=1m requires fidelity>=10)\n            intervalForFidelity = self.safe_string(request, 'interval')\n            if intervalForFidelity == '1m':\n                finalFidelity = max(10, finalFidelity)\n            request['fidelity'] = finalFidelity\n        remainingParams = self.extend({'api_type': 'clob'}, self.extend(request, self.omit(params, ['market', 'startTs', 'endTs', 'fidelity', 'interval'])))\n        return await self.request('prices-history', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_time(self, params={}):\n        \"\"\"\n        fetches the current server timestamp from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L178\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # Based on get_server_time() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(TIME = \"/time\")\n        return await self.request('time', ['clob', 'public'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_public_get_ok(self, params={}):\n        \"\"\"\n        health check endpoint to confirm server is up\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # Based on get_ok() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n        return await self.request('', ['clob', 'public'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_public_get_fee_rate(self, params={}):\n        \"\"\"\n        fetches the fee rate for a token from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetFeeRate() requires a token_id parameter')\n        # Based on get_fee_rate() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_FEE_RATE = \"/fee-rate\")\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('fee-rate', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_price(self, params={}):\n        \"\"\"\n        fetches the market price for a specific token and side from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-market-price\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :param str [params.side]: the side: 'BUY' or 'SELL'(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetPrice() requires a token_id parameter')\n        side = self.safe_string(params, 'side')\n        if side is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetPrice() requires a side parameter(BUY or SELL)')\n        # Note: REST API endpoint format: /price?token_id={token_id}&side={side}\n        # See https://docs.polymarket.com/api-reference/pricing/get-market-price\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('price', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_prices(self, params={}):\n        \"\"\"\n        fetches market prices for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch prices for\n        :param str [params.side]: the side: 'BUY' or 'SELL'(required if token_ids provided)\n        :returns dict: response from the exchange\n        \"\"\"\n        # Note: REST API endpoint format: /prices?token_id={token_id1,token_id2,...}\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices\n        # Response format: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        # The endpoint returns both BUY and SELL prices for each token_id\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('prices', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_post_prices(self, params={}):\n        \"\"\"\n        fetches market prices for specified tokens and sides via POST request\n\n        https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param Array [params.requests]: array of {token_id, side} objects(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        requests = self.safe_value(params, 'requests')\n        if requests is None:\n            raise ArgumentsRequired(self.id + ' clobPublicPostPrices() requires a requests parameter(array of {token_id, side} objects)')\n        # Note: REST API endpoint format: POST /prices with JSON body\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        # Body format: [{\"token_id\": \"1234567890\", \"side\": \"BUY\"}, {\"token_id\": \"1234567890\", \"side\": \"SELL\"}]\n        # Response format: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        body = self.json(requests)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'requests'))\n        return await self.request('prices', ['clob', 'public'], 'POST', remainingParams, None, body)\n\n    async def clob_public_get_midpoint(self, params={}):\n        \"\"\"\n        fetches the midpoint price for a specific token from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-midpoint-price\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetMidpoint() requires a token_id parameter')\n        # Note: REST API endpoint format: /midpoint?token_id={token_id}\n        # See https://docs.polymarket.com/api-reference/pricing/get-midpoint-price\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('midpoint', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_midpoints(self, params={}):\n        \"\"\"\n        fetches midpoint prices for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-midpoint-prices\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch midpoints for(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenIds = self.safe_value(params, 'token_ids')\n        if tokenIds is None or not isinstance(tokenIds, list) or len(tokenIds) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicGetMidpoints() requires a token_ids parameter(array of token IDs)')\n        # Note: REST API endpoint format: POST /midpoints with JSON body\n        # See https://docs.polymarket.com/api-reference/pricing/get-midpoint-prices\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: {[token_id]: \"midpoint\", ...}\n        body: List[Any] = []\n        for i in range(0, len(tokenIds)):\n            body.append({'token_id': tokenIds[i]})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'token_ids'))\n        return await self.request('midpoints', ['clob', 'public'], 'POST', remainingParams, None, self.json(body))\n\n    async def clob_public_get_spread(self, params={}):\n        \"\"\"\n        fetches the bid-ask spread for a specific token from CLOB API\n\n        https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spread\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetSpread() requires a token_id parameter')\n        # Note: REST API endpoint format: /spread?token_id={token_id}\n        # See https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spread\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('spread', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_last_trade_price(self, params={}):\n        \"\"\"\n        fetches the last trade price for a specific token from CLOB API\n\n        https://docs.polymarket.com/api-reference/trades/get-last-trade-price\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetLastTradePrice() requires a token_id parameter')\n        # Note: REST API endpoint format: /last-trade-price?token_id={token_id}\n        # See https://docs.polymarket.com/api-reference/trades/get-last-trade-price\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('last-trade-price', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_last_trades_prices(self, params={}):\n        \"\"\"\n        fetches last trade prices for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/trades/get-last-trades-prices\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch last trade prices for(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenIds = self.safe_value(params, 'token_ids')\n        if tokenIds is None or not isinstance(tokenIds, list) or len(tokenIds) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicGetLastTradesPrices() requires a token_ids parameter(array of token IDs)')\n        # Note: REST API endpoint format: POST /last-trades-prices with JSON body\n        # See https://docs.polymarket.com/api-reference/trades/get-last-trades-prices\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: {[token_id]: \"price\", ...}\n        body: List[Any] = []\n        for i in range(0, len(tokenIds)):\n            body.append({'token_id': tokenIds[i]})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'token_ids'))\n        return await self.request('last-trades-prices', ['clob', 'public'], 'POST', remainingParams, None, self.json(body))\n\n    async def clob_public_get_trades(self, params={}):\n        \"\"\"\n        fetches trades for a specific market from CLOB API\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#trades\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: the token ID or condition ID(required)\n        :param int [params.limit]: maximum number of trades to return(default: 100, max: 500)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :param int [params.start_timestamp]: start timestamp in seconds\n        :param int [params.end_timestamp]: end timestamp in seconds\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_string(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetTrades() requires a market(token_id or condition_id) parameter')\n        # Note: REST API endpoint format: /trades?market={token_id}\n        # See https://docs.polymarket.com/developers/CLOB/clients/methods-public#trades\n        request: dict = {\n            'market': market,\n        }\n        limit = self.safe_integer(params, 'limit')\n        if limit is not None:\n            request['limit'] = min(limit, 500)  # Cap at 500\n        side = self.safe_string(params, 'side')\n        if side is not None:\n            request['side'] = side\n        startTimestamp = self.safe_integer(params, 'start_timestamp')\n        if startTimestamp is not None:\n            request['start_timestamp'] = startTimestamp\n        endTimestamp = self.safe_integer(params, 'end_timestamp')\n        if endTimestamp is not None:\n            request['end_timestamp'] = endTimestamp\n        remainingParams = self.extend({'api_type': 'clob'}, self.extend(request, self.omit(params, ['market', 'limit', 'side', 'start_timestamp', 'end_timestamp'])))\n        return await self.request('trades', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_tick_size(self, params={}):\n        \"\"\"\n        fetches the tick size for a token from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetTickSize() requires a token_id parameter')\n        # Based on get_tick_size() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_TICK_SIZE = \"/tick-size\")\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('tick-size', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_get_neg_risk(self, params={}):\n        \"\"\"\n        fetches the negative risk flag for a token from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetNegRisk() requires a token_id parameter')\n        # Based on get_neg_risk() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_NEG_RISK = \"/neg-risk\")\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return await self.request('neg-risk', ['clob', 'public'], 'GET', remainingParams)\n\n    async def clob_public_post_spreads(self, params={}):\n        \"\"\"\n        fetches bid-ask spreads for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spreads\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch spreads for(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenIds = self.safe_value(params, 'token_ids')\n        if tokenIds is None or not isinstance(tokenIds, list) or len(tokenIds) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicPostSpreads() requires a token_ids parameter(array of token IDs)')\n        # Note: REST API endpoint format: POST /spreads\n        # See https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spreads\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: {[token_id]: \"spread\", ...}\n        body: List[Any] = []\n        for i in range(0, len(tokenIds)):\n            body.append({'token_id': tokenIds[i]})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'token_ids'))\n        return await self.request('spreads', ['clob', 'public'], 'POST', remainingParams, None, self.json(body))\n\n    async def clob_private_get_order(self, params={}):\n        \"\"\"\n        fetches a specific order by order ID\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_ORDER = \"/data/order/\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderId = self.safe_string(params, 'order_id')\n        if orderId is None:\n            raise ArgumentsRequired(self.id + ' clobPrivateGetOrder() requires an order_id parameter')\n        path = 'data/order/' + self.encode_uri_component(orderId)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'order_id'))\n        return await self.request(path, ['clob', 'private'], 'GET', remainingParams)\n\n    async def clob_private_get_orders(self, params={}):\n        \"\"\"\n        fetches orders for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(ORDERS = \"/data/orders\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: filter orders by token ID\n        :param str [params.status]: filter orders by status(OPEN, FILLED, CANCELLED, etc.)\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('data/orders', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_post_order(self, params={}):\n        \"\"\"\n        creates a new order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(POST_ORDER = \"/order\")\n        https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param dict [params.order]: order object(required)\n        :param str [params.owner]: api key of order owner(required)\n        :param str [params.orderType]: order type: \"FOK\", \"GTC\", \"GTD\"(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        # Build request payload according to API specification\n        # See https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n        order = self.safe_value(params, 'order')\n        if order is None:\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrder() requires an order parameter')\n        owner = self.safe_string(params, 'owner')\n        if owner is None:\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrder() requires an owner parameter(API key)')\n        orderType = self.safe_string(params, 'orderType')\n        if orderType is None:\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrder() requires an orderType parameter')\n        # Build the complete request payload with top-level fields\n        requestPayload: dict = {\n            'order': order,\n            'owner': owner,\n            'orderType': orderType,\n        }\n        # Add optional parameters if provided\n        clientOrderId = self.safe_string(params, 'clientOrderId')\n        if clientOrderId is not None:\n            requestPayload['clientOrderId'] = clientOrderId\n        postOnly = self.safe_bool(params, 'postOnly')\n        if postOnly is not None:\n            requestPayload['postOnly'] = postOnly\n        # Send the complete request payload body\n        body = self.json(requestPayload)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, ['order', 'owner', 'orderType', 'clientOrderId', 'postOnly']))\n        return await self.request('order', ['clob', 'private'], 'POST', remainingParams, None, body)\n\n    async def clob_private_post_orders(self, params={}):\n        \"\"\"\n        creates multiple orders in a batch\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(POST_ORDERS = \"/orders\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param Array [params.orders]: array of order objects(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orders = self.safe_value(params, 'orders')\n        if orders is None or not isinstance(orders, list):\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrders() requires an orders parameter(array of order objects)')\n        body = self.json(orders)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'orders'))\n        return await self.request('orders', ['clob', 'private'], 'POST', remainingParams, None, body)\n\n    async def clob_private_delete_order(self, params={}):\n        \"\"\"\n        cancels an order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL = \"/order\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID to cancel(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderId = self.safe_string(params, 'order_id')\n        if orderId is None:\n            raise ArgumentsRequired(self.id + ' clobPrivateDeleteOrder() requires an order_id parameter')\n        request: dict = {\n            'orderID': orderId,\n        }\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'order_id'))\n        body = self.json(request)\n        return await self.request('order', ['clob', 'private'], 'DELETE', remainingParams, None, body)\n\n    async def clob_private_delete_orders(self, params={}):\n        \"\"\"\n        cancels multiple orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ORDERS = \"/orders\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.order_ids]: array of order IDs to cancel(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderIds = self.safe_value(params, 'order_ids')\n        if orderIds is None or not isinstance(orderIds, list):\n            raise ArgumentsRequired(self.id + ' clobPrivateDeleteOrders() requires an order_ids parameter(array of order IDs)')\n        body = self.json(orderIds)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'order_ids'))\n        return await self.request('orders', ['clob', 'private'], 'DELETE', remainingParams, None, body)\n\n    async def clob_private_delete_cancel_all(self, params={}):\n        \"\"\"\n        cancels all open orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ALL = \"/cancel-all\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: optional token ID to cancel all orders for a specific market\n        :returns dict: response from the exchange\n        \"\"\"\n        body = self.json(params)\n        return await self.request('cancel-all', ['clob', 'private'], 'DELETE', {'api_type': 'clob'}, None, body)\n\n    async def clob_private_delete_cancel_market_orders(self, params={}):\n        \"\"\"\n        cancels all orders from a market\n\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-market-orders\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: condition id of the market\n        :param str [params.asset_id]: id of the asset/token\n        :returns dict: response from the exchange\n        \"\"\"\n        request: dict = {}\n        market = self.safe_string(params, 'market')\n        if market is not None:\n            request['market'] = market\n        assetId = self.safe_string(params, 'asset_id')\n        if assetId is not None:\n            request['asset_id'] = assetId\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, ['market', 'asset_id']))\n        body = self.json(request)\n        return await self.request('cancel-market-orders', ['clob', 'private'], 'DELETE', remainingParams, None, body)\n\n    async def clob_private_get_trades(self, params={}):\n        \"\"\"\n        fetches trade history for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py(get_trades method)\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: filter trades by token ID\n        :param int [params.start_timestamp]: start timestamp in seconds\n        :param str [params.next_cursor]: pagination cursor\n        :returns dict: response from the exchange\n        \"\"\"\n        # NOTE: the authenticated L2 endpoint is `/trades`(without the public `/data/` prefix).\n        # Using the public path would return all market trades instead of the caller's own fills.\n        return await self.request('trades', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_get_builder_trades(self, params={}):\n        \"\"\"\n        fetches trades originated by the builder\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_BUILDER_TRADES = \"/builder-trades\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: filter trades by token ID\n        :param int [params.start_timestamp]: start timestamp in seconds\n        :param str [params.next_cursor]: pagination cursor\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('builder-trades', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_get_notifications(self, params={}):\n        \"\"\"\n        fetches notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_NOTIFICATIONS = \"/notifications\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('notifications', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_delete_notifications(self, params={}):\n        \"\"\"\n        drops notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(DROP_NOTIFICATIONS = \"/notifications\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.notification_id]: specific notification ID to drop\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('notifications', ['clob', 'private'], 'DELETE', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_get_balance_allowance(self, params={}):\n        \"\"\"\n        fetches balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_BALANCE_ALLOWANCE = \"/balance-allowance\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        return await self.request('balance-allowance', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_put_balance_allowance(self, params={}):\n        \"\"\"\n        updates balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(UPDATE_BALANCE_ALLOWANCE = \"/balance-allowance\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        body = self.json(params)\n        return await self.request('balance-allowance', ['clob', 'private'], 'PUT', {'api_type': 'clob'}, None, body)\n\n    async def clob_private_get_is_order_scoring(self, params={}):\n        \"\"\"\n        checks if an order is currently scoring\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(IS_ORDER_SCORING = \"/is-order-scoring\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID(required)\n        :param str [params.token_id]: the token ID(required)\n        :param str [params.side]: the side: 'BUY' or 'SELL'(required)\n        :param str [params.price]: the price(required)\n        :param str [params.size]: the size(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        # GET /order-scoring?order_id=...\n        return await self.request('order-scoring', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    async def clob_private_post_are_orders_scoring(self, params={}):\n        \"\"\"\n        checks if multiple orders are currently scoring\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(ARE_ORDERS_SCORING = \"/are-orders-scoring\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.orderIds]: array of order IDs to check(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderIds = self.safe_value_2(params, 'orderIds', 'order_ids')\n        if orderIds is None or not isinstance(orderIds, list):\n            raise ArgumentsRequired(self.id + ' clobPrivatePostAreOrdersScoring() requires an orderIds parameter(array of order IDs)')\n        body = self.json({'orderIds': orderIds})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, ['orderIds', 'order_ids']))\n        # POST /orders-scoring with JSON body {orderIds: [...]}\n        return await self.request('orders-scoring', ['clob', 'private'], 'POST', remainingParams, None, body)\n\n    def get_main_wallet_address(self):\n        \"\"\"\n        gets main wallet address(walletAddress or options.funder)\n        :returns str: main wallet address\n        \"\"\"\n        if self.walletAddress is not None and self.walletAddress != '':\n            return self.walletAddress\n        funder = self.safe_string(self.options, 'funder')\n        if funder is not None and funder != '':\n            return funder\n        raise ArgumentsRequired(self.id + ' getMainWalletAddress() requires a wallet address. Set `walletAddress` or `options.funder`.')\n\n    def get_proxy_wallet_address(self):\n        \"\"\"\n        gets proxy wallet address for Data-API endpoints(falls back to main wallet if not set)\n        :returns str: proxy wallet address\n        \"\"\"\n        if self.uid is not None and self.uid != '':\n            return self.uid\n        proxyWallet = self.safe_string(self.options, 'proxyWallet')\n        if proxyWallet is not None and proxyWallet != '':\n            return proxyWallet\n        # Fall back to main wallet if proxyWallet is not set\n        return self.get_main_wallet_address()\n\n    def get_builder_wallet_address(self):\n        \"\"\"\n        gets builder wallet address(falls back to main wallet if not set)\n        :returns str: builder wallet address\n        \"\"\"\n        builderWallet = self.safe_string(self.options, 'builderWallet')\n        if builderWallet is not None and builderWallet != '':\n            return builderWallet\n        # Fall back to main wallet if builderWallet is not set\n        return self.get_main_wallet_address()\n\n    async def get_user_total_value(self, userAddress: str = None) -> dict:\n        \"\"\"\n        fetches total value of a user's positions from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-total-value-of-a-users-positions\n\n        :param str [userAddress]: user wallet address(defaults to getProxyWalletAddress())\n        :returns dict: object with 'value'(number) and 'response'(raw API response)\n        \"\"\"\n        address: str = None\n        if userAddress is not None:\n            # Use provided address directly(public endpoint, no wallet setup needed)\n            address = userAddress\n        else:\n            # Try to get proxy wallet address, but handle case where wallet is not configured\n            # This allows public calls without requiring wallet setup\n            try:\n                address = self.get_proxy_wallet_address()\n            except Exception as e:\n                # If wallet is not configured, require userAddress parameter for public calls\n                raise ArgumentsRequired(self.id + ' getUserTotalValue() requires a userAddress parameter when wallet is not configured. This is a public endpoint that can query any user address.')\n        # Fetch total value from Data-API\n        valueResponse = await self.data_public_get_total_value({'user': address})\n        # Response format: [{\"user\": \"0x...\", \"value\": 123}]\n        valueData = valueResponse\n        if isinstance(valueResponse, list):\n            if len(valueResponse) > 0:\n                valueData = valueResponse[0]\n            else:\n                valueData = {}\n        totalValue = self.safe_number(valueData, 'value', 0)\n        return {\n            'value': totalValue,\n            'response': valueResponse,\n        }\n\n    async def get_user_positions(self, userAddress: str = None, params={}) -> dict:\n        \"\"\"\n        fetches current positions for a user from Data-API(defaults to proxy wallet)\n\n        https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user\n\n        :param str [userAddress]: user wallet address(defaults to getProxyWalletAddress())\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # TODO add pagination, sort, limit etc https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user\n        address: str = None\n        if userAddress is not None:\n            # Use provided address directly(public endpoint, no wallet setup needed)\n            address = userAddress\n        else:\n            # Try to get proxy wallet address, but handle case where wallet is not configured\n            # This allows public calls without requiring wallet setup\n            try:\n                address = self.get_proxy_wallet_address()\n            except Exception as e:\n                # If wallet is not configured, require userAddress parameter for public calls\n                raise ArgumentsRequired(self.id + ' getUserPositions() requires a userAddress parameter when wallet is not configured. This is a public endpoint that can query any user address.')\n        return await self.data_public_get_positions(self.extend({'user': address}, params))\n\n    async def get_user_activity(self, userAddress: str = None, params={}) -> dict:\n        \"\"\"\n        fetches user activity from Data-API(defaults to proxy wallet)\n\n        https://docs.polymarket.com/api-reference/core/get-user-activity\n\n        :param str [userAddress]: user wallet address(defaults to getProxyWalletAddress())\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        address: str = None\n        if userAddress is not None:\n            # Use provided address directly(public endpoint, no wallet setup needed)\n            address = userAddress\n        else:\n            # Try to get proxy wallet address, but handle case where wallet is not configured\n            # This allows public calls without requiring wallet setup\n            try:\n                address = self.get_proxy_wallet_address()\n            except Exception as e:\n                # If wallet is not configured, require userAddress parameter for public calls\n                raise ArgumentsRequired(self.id + ' getUserActivity() requires a userAddress parameter when wallet is not configured. This is a public endpoint that can query any user address.')\n        request: dict = {\n            'user': address,\n            'limit': self.safe_integer(params, 'limit', 100),\n            'offset': self.safe_integer(params, 'offset', 0),\n            'sortBy': self.safe_string(params, 'sortBy', 'TIMESTAMP'),\n            'sortDirection': self.safe_string(params, 'sortDirection', 'DESC'),\n        }\n        return await self.data_public_get_activity(self.extend(request, self.omit(params, ['user'])))\n\n    def parse_user_activity(self, activity: dict, market: Market = None) -> dict:\n        \"\"\"\n        parse a raw user activity record into a trade-like structure consumable by parseTrades\n        :param dict activity: raw activity payload from Data-API\n        :param dict [market]: market structure, when known\n        :returns dict|None: normalized activity(only for TRADE records) or None\n        \"\"\"\n        activityType = self.safe_string(activity, 'type')\n        if activityType != 'TRADE':\n            return None\n        rawTs = self.safe_integer(activity, 'timestamp')\n        isoTimestamp = self.safe_string(activity, 'timestamp')\n        if rawTs is not None:\n            tsMs = rawTs * 1000 if (rawTs < 1000000000000) else rawTs\n            isoTimestamp = self.iso8601(tsMs)\n        symbol = market['symbol'] if (market is not None) else self.safe_string(activity, 'condition_id')\n        return self.extend(activity, {\n            'timestamp': isoTimestamp,\n            'transactionHash': self.safe_string(activity, 'transactionHash'),\n            'symbol': symbol,\n            'asset': self.safe_string(activity, 'asset'),\n            'price': self.safe_number(activity, 'price'),\n            'size': self.safe_number(activity, 'size'),\n            'side': self.safe_string(activity, 'side'),\n        })\n\n    def format_address(self, address: str = None):\n        if address is None:\n            return None\n        if address.startswith('0x'):\n            return address.replace('0x', '')\n        return address\n\n    def normalize_address(self, address: str) -> str:\n        normalized = str(address).strip()\n        if not normalized.startswith('0x'):\n            normalized = '0x' + normalized\n        return normalized.lower()\n\n    def hash_message(self, message: str) -> str:\n        binaryMessage = self.encode(message)\n        binaryMessageLength = self.binary_length(binaryMessage)\n        x19 = self.base16_to_binary('19')\n        newline = self.base16_to_binary('0a')\n        prefix = self.binary_concat(x19, self.encode('Ethereum Signed Message:'), newline, self.encode(self.number_to_string(binaryMessageLength)))\n        return '0x' + self.hash(self.binary_concat(prefix, binaryMessage), 'keccak', 'hex')\n\n    def get_contract_config(self, chainID: float) -> dict:\n        contracts = self.safe_value(self.options, 'contracts', {})\n        chainIdStr = str(chainID)\n        contractConfig = self.safe_value(contracts, chainIdStr)\n        if contractConfig is None:\n            raise ExchangeError(self.id + ' getContractConfig() invalid network chainId: ' + chainIdStr)\n        return contractConfig\n\n    def sign_message(self, message: str, privateKey: str) -> str:\n        hash = self.hash_message(message)\n        return self.sign_hash(hash, privateKey)\n\n    def sign_hash(self, hash: str, privateKey: str):\n        signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None)\n        r = signature['r']\n        s = signature['s']\n        v = self.int_to_base16(self.sum(27, signature['v']))\n        # Convert to lowercase hex(Ethereum standard)\n        finalSignature = ('0x' + r.rjust(64, '0') + s.rjust(64, '0') + v.rjust(2, '0')).lower()\n        return finalSignature\n\n    def sign_typed_data(self, domain: dict, types: dict, value: dict) -> str:\n        # This returns binary data: 0x1901 or hashDomain(domain) or hashStruct(primaryType, types, value)\n        encoded = self.eth_encode_structured_data(domain, types, value)\n        # Hash the encoded binary data with keccak256\n        hash = '0x' + self.hash(encoded, 'keccak', 'hex')\n        # Sign the hash using signHash\n        signature = self.sign_hash(hash, self.privateKey)\n        return signature\n\n    def create_level1_headers(self, walletAddress: str, nonce: float = None) -> dict:\n        if walletAddress is None or walletAddress == '':\n            raise ArgumentsRequired(self.id + ' createLevel1Headers() requires a valid walletAddress')\n        normalizedAddress = self.normalize_address(walletAddress)\n        chainId = self.safe_integer(self.options, 'chainId')\n        timestampSeconds = int(math.floor(self.milliseconds()) / 1000)\n        timestamp = str(timestampSeconds)\n        nonceValue = 0\n        if nonce is not None:\n            nonceValue = nonce\n        clobDomainName = self.safe_string(self.options, 'clobDomainName')\n        clobVersion = self.safe_string(self.options, 'clobVersion')\n        msgToSign = self.safe_string(self.options, 'msgToSign')\n        domain = {\n            'name': clobDomainName,\n            'version': clobVersion,\n            'chainId': chainId,\n        }\n        # https://github.com/Polymarket/clob-client/blob/b75aec68be17190215b7230372fbedfe85de20ef/src/signing/eip712.ts#L28\n        types = {\n            'ClobAuth': [\n                {'name': 'address', 'type': 'address'},\n                {'name': 'timestamp', 'type': 'string'},\n                {'name': 'nonce', 'type': 'uint256'},\n                {'name': 'message', 'type': 'string'},\n            ],\n        }\n        message = {\n            'address': normalizedAddress,\n            'timestamp': timestamp,\n            'nonce': nonceValue,\n            'message': msgToSign,\n        }\n        signature = self.sign_typed_data(domain, types, message)\n        headers = {\n            'POLY_ADDRESS': normalizedAddress,\n            'POLY_TIMESTAMP': timestamp,\n            'POLY_NONCE': str(nonceValue),\n            'POLY_SIGNATURE': signature,\n        }\n        return headers\n\n    def get_clob_base_url(self, params={}) -> str:\n        \"\"\"\n        Gets the CLOB API base URL(handles sandbox mode and custom hosts)\n        :param dict [params]: extra parameters\n        :returns str: base URL for CLOB API\n        \"\"\"\n        apiType = self.safe_string(params, 'api_type', 'clob')\n        baseUrl = self.urls['api'][apiType]\n        # Check for sandbox mode\n        if self.isSandboxModeEnabled and self.urls['test'] and self.urls['test'][apiType]:\n            baseUrl = self.urls['test'][apiType]\n        if apiType == 'clob':\n            customHost = self.safe_string(self.options, 'clobHost')\n            if customHost is not None:\n                baseUrl = customHost\n        return baseUrl\n\n    def parse_api_credentials(self, response: Any) -> dict:\n        \"\"\"\n        Parses API credentials from API response and caches them\n        :param dict response: API response\n        :returns dict} API credentials {apiKey, secret, passphrase:\n        \"\"\"\n        apiKey = self.safe_string(response, 'apiKey') or self.safe_string(response, 'api_key')\n        secret = self.safe_string(response, 'secret')\n        passphrase = self.safe_string(response, 'passphrase')\n        if not apiKey or not secret or not passphrase:\n            raise ExchangeError(self.id + ' parseApiCredentials() failed to parse credentials. Response: ' + self.json(response))\n        credentials = {\n            'apiKey': apiKey,\n            'secret': secret,\n            'passphrase': passphrase,\n        }\n        # Cache credentials in options\n        self.options['apiCredentials'] = credentials\n        # Also set them properties for use in sign() method\n        self.apiKey = apiKey\n        self.secret = secret\n        self.password = passphrase\n        return credentials\n\n    async def create_api_key(self, params={}) -> dict:\n        \"\"\"\n        Creates a new CLOB API key for the given address\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param number [params.nonce]: optional nonce/timestamp\n        :returns dict} API credentials {apiKey, secret, passphrase:\n @note Uses manual URL building instead of self.request() because self endpoint requires L1 authentication\n (POLY_ADDRESS, POLY_SIGNATURE headers) rather than the standard L2 authentication used by self.request()\n        \"\"\"\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' create_api_key() requires a privateKey')\n        # Validate privateKey format(should be hex string with 0x prefix, 66 chars total)\n        if not self.privateKey.startswith('0x') or len(self.privateKey) != 66:\n            raise ArgumentsRequired(self.id + ' create_api_key() requires a valid privateKey(0x-prefixed hex string, 66 characters)')\n        walletAddress = self.get_main_wallet_address()\n        # Validate walletAddress format(should be hex string with 0x prefix, 42 chars total)\n        if not walletAddress.startswith('0x') or len(walletAddress) != 42:\n            raise ArgumentsRequired(self.id + ' create_api_key() requires a valid walletAddress(0x-prefixed hex string, 42 characters). Got: ' + walletAddress)\n        baseUrl = self.get_clob_base_url(params)\n        nonce = self.safe_integer(params, 'nonce')\n        headers = self.create_level1_headers(walletAddress, nonce)\n        url = baseUrl + '/auth/api-key'\n        # POST /auth/api-key(creates new API credentials with L1 authentication)\n        response = await self.fetch(url, 'POST', headers, None)\n        return self.parse_api_credentials(response)\n\n    async def derive_api_key(self, params={}) -> dict:\n        \"\"\"\n        Derives an already existing CLOB API key for the given address and nonce\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param number [params.nonce]: optional nonce/timestamp\n        :returns dict} API credentials {apiKey, secret, passphrase:\n @note Uses manual URL building instead of self.request() because self endpoint requires L1 authentication\n (POLY_ADDRESS, POLY_SIGNATURE headers) rather than the standard L2 authentication used by self.request()\n        \"\"\"\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' derive_api_key() requires a privateKey')\n        walletAddress = self.get_main_wallet_address()\n        baseUrl = self.get_clob_base_url(params)\n        nonce = self.safe_integer(params, 'nonce')\n        headers = self.create_level1_headers(walletAddress, nonce)\n        url = baseUrl + '/auth/derive-api-key'\n        # GET /auth/derive-api-key(derives existing API credentials with L1 authentication)\n        response = await self.fetch(url, 'GET', headers, None)\n        return self.parse_api_credentials(response)\n\n    async def create_or_derive_api_creds(self, params={}) -> dict:\n        \"\"\"\n        Creates API creds if not already created for nonce, otherwise derives them\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param number [params.nonce]: optional nonce/timestamp\n        :returns dict} API credentials {apiKey, secret, passphrase:\n        \"\"\"\n        # Check if credentials are already cached\n        cachedCreds = self.safe_dict(self.options, 'apiCredentials')\n        if cachedCreds is not None:\n            return cachedCreds\n        # Try create_api_key first, then derive_api_key if create fails\n        # Based on py-clob-client client.py: create_or_derive_api_creds()\n        try:\n            return await self.create_api_key(params)\n        except Exception as e:\n            # If create fails(e.g., key already exists), try to derive it\n            return await self.derive_api_key(params)\n\n    def set_api_creds(self, credentials: dict):\n        \"\"\"\n        Sets API credentials(alias for caching credentials)\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict credentials: API credentials {apiKey, secret, passphrase}\n        \"\"\"\n        self.options['apiCredentials'] = credentials\n        self.apiKey = self.safe_string(credentials, 'apiKey')\n        self.secret = self.safe_string(credentials, 'secret')\n        self.password = self.safe_string(credentials, 'passphrase')\n\n    def get_api_base_url(self, params={}) -> str:\n        \"\"\"\n        Gets the API base URL for the specified API type(handles sandbox mode and custom hosts)\n        :param dict [params]: extra parameters\n        :param str [params.api_type]: API type('clob', 'gamma', 'data', etc.)\n        :returns str: base URL for the API\n        \"\"\"\n        apiType = self.safe_string(params, 'api_type', 'clob')\n        # Ensure urls.api exists\n        if self.urls is None or self.urls['api'] is None:\n            raise ExchangeError(self.id + ' getApiBaseUrl() failed: urls.api is not initialized. Make sure exchange is properly initialized.')\n        # Direct access to nested object property\n        baseUrl = self.urls['api'][apiType]\n        # Check for sandbox mode\n        if self.isSandboxModeEnabled and self.urls['test'] and self.urls['test'][apiType]:\n            baseUrl = self.urls['test'][apiType]\n        # Allow custom CLOB host override\n        if apiType == 'clob':\n            customHost = self.safe_string(self.options, 'clobHost')\n            if customHost is not None:\n                baseUrl = customHost\n        # Ensure we have a valid base URL\n        if baseUrl is None:\n            apiUrls = self.urls['api'] or {}\n            availableTypesList = list(apiUrls.keys())\n            availableTypes = ''\n            if len(availableTypesList) > 0:\n                availableTypes = ', '.join(availableTypesList)\n            raise ExchangeError(self.id + ' getApiBaseUrl() failed: API type \"' + apiType + '\" not found in urls.api. Available types: ' + availableTypes)\n        return baseUrl\n\n    def build_default_headers(self, method: str, existingHeaders: dict = None) -> dict:\n        \"\"\"\n        Builds default HTTP headers based on py-clob-client helpers.py\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/http_helpers/helpers.py\n\n        :param str method: HTTP method('GET', 'POST', etc.)\n        :param dict [existingHeaders]: existing headers to self.extend\n        :returns dict: headers dictionary\n        \"\"\"\n        if existingHeaders is None:\n            existingHeaders = {}\n        headers = self.extend({\n            'User-Agent': 'ccxt',\n            'Accept': '*/*',\n            'Connection': 'keep-alive',\n            'Content-Type': 'application/json',\n        }, existingHeaders)\n        # Add Accept-Encoding for GET requests(as per py-clob-client)\n        if method == 'GET':\n            headers['Accept-Encoding'] = 'gzip'\n        return headers\n\n    def build_public_request(self, baseUrl: str, pathWithParams: str, method: str, queryParams: dict, body: str = None, headers: dict = None) -> dict:\n        \"\"\"\n        Builds a public(unauthenticated) request\n        :param str baseUrl: API base URL\n        :param str pathWithParams: path with parameters\n        :param str method: HTTP method\n        :param dict queryParams: query parameters\n        :param str [body]: request body\n        :param dict [headers]: request headers\n        :returns dict: request object with url, method, body, and headers\n        \"\"\"\n        headers = self.build_default_headers(method, headers)\n        url = baseUrl + '/' + pathWithParams\n        if method == 'GET':\n            if queryParams:\n                url += '?' + self.urlencode(queryParams)\n        else:\n            # For POST requests, body should already be set by the calling method\n            if body is None and queryParams:\n                body = self.json(queryParams)\n        return {'url': url, 'method': method, 'body': body, 'headers': headers}\n\n    async def ensure_api_credentials(self, params={}) -> dict:\n        \"\"\"\n        Ensures API credentials are generated(lazy generation, similar to dYdX's retrieveCredentials)\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict} API credentials {apiKey, secret, passphrase:\n        \"\"\"\n        # Check if credentials are already cached\n        cachedCreds = self.safe_dict(self.options, 'apiCredentials')\n        if cachedCreds is not None:\n            return cachedCreds\n        # Check if credentials are provided directly(apiKey, secret, password)\n        # This allows users to provide credentials directly instead of generating from privateKey\n        if self.apiKey and self.secret and self.password:\n            directCreds = {\n                'apiKey': self.apiKey,\n                'secret': self.secret,\n                'passphrase': self.password,\n            }\n            self.set_api_creds(directCreds)\n            return directCreds\n        # If direct credentials not provided, check if privateKey is available for generation\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' ensureApiCredentials() requires either: (1) apiKey + secret + password provided directly, or (2) privateKey to generate credentials')\n        # Generate credentials lazily(similar to dYdX's retrieveCredentials pattern)\n        # This is called automatically before authenticated requests\n        creds = await self.create_or_derive_api_creds(params)\n        self.set_api_creds(creds)\n        return creds\n\n    def get_api_credentials(self) -> dict:\n        \"\"\"\n        Gets API credentials from cache or instance properties\n        :returns dict} API credentials {apiKey, secret, password:\n        \"\"\"\n        apiKey = self.apiKey\n        secret = self.secret\n        password = self.password\n        # Check if credentials are already cached\n        cachedCreds = self.safe_dict(self.options, 'apiCredentials')\n        if cachedCreds is not None:\n            apiKey = self.safe_string(cachedCreds, 'apiKey') or apiKey\n            secret = self.safe_string(cachedCreds, 'secret') or secret\n            password = self.safe_string(cachedCreds, 'passphrase') or password\n        # If credentials are not available, check if privateKey is set\n        # Only raise error if privateKey is set(meaning user wants authenticated requests)\n        # This allows public requests to work even when privateKey is set but credentials not yet generated\n        if not apiKey or not secret or not password:\n            if self.privateKey is None:\n                # No privateKey set - self should not happen if called from buildPrivateRequest\n                raise ArgumentsRequired(self.id + ' getApiCredentials() called but no credentials available and no privateKey set. This should only be called for authenticated requests. Provide either: (1) apiKey + secret + password directly, or (2) privateKey to generate credentials.')\n            # privateKey is set but credentials not generated yet - self is expected for lazy generation\n            # Don't raise error here, ensureApiCredentials() handle it\n            raise ArgumentsRequired(self.id + ' API credentials not generated. Credentials are automatically generated on first authenticated request, but privateKey is required. Alternatively, provide apiKey + secret + password directly.')\n        return {'apiKey': apiKey, 'secret': secret, 'password': password}\n\n    def build_request_path_and_payload(self, pathWithParams: str, method: str, queryParams: dict, body: str = None) -> dict:\n        \"\"\"\n        Builds the request path and payload for signature\n        :param str pathWithParams: path with parameters\n        :param str method: HTTP method\n        :param dict queryParams: query parameters\n        :param str [body]: request body\n        :returns dict} {requestPath, url, payload, body:\n        \"\"\"\n        # Ensure path doesn't have double slashes(pathWithParams may already start with /)\n        normalizedPath = pathWithParams if pathWithParams.startswith('/') else '/' + pathWithParams\n        requestPath = normalizedPath\n        url = requestPath\n        payload = ''\n        if method == 'GET':\n            if queryParams:\n                queryString = self.urlencode(queryParams)\n                url += '?' + queryString\n                payload = queryString\n        else:\n            # For POST/PUT/DELETE, body is part of the signature\n            # Use deterministic JSON serialization(no spaces, compact) matching py-clob-client\n            # json.dumps(body, separators=(\",\", \":\"), ensure_ascii=False) produces compact JSON\n            if body is None and queryParams:\n                # json.dumpsby default produces compact JSON(no spaces)\n                body = json.dumps(queryParams)\n            # Serialize body deterministically if it's an object\n            if body is not None and isinstance(body, dict):\n                body = json.dumps(body)\n            # Use body(quote replacement happens in createLevel2Signature)\n            payload = str(body) if (body is not None and body != '') else ''\n        return {'requestPath': requestPath, 'url': url, 'payload': payload, 'body': body}\n\n    def create_level2_signature(self, timestamp: str, method: str, requestPath: str, body: str, secret: str) -> str:\n        \"\"\"\n        Creates Level 2 authentication signature(HMAC-SHA256)\n\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param str timestamp: timestamp string\n        :param str method: HTTP method\n        :param str requestPath: request path\n        :param str body: request body(serialized JSON string)\n        :param str secret: API secret(base64 encoded, URL-safe)\n        :returns str: URL-safe base64 encoded signature\n        \"\"\"\n        # Create signature: HMAC-SHA256(timestamp + method + path + body, secret)\n        # Based on Polymarket CLOB API L2 authentication(matches py-clob-client build_hmac_signature)\n        # Use str(method) to preserve case(don't use toUpperCase())\n        message = str(timestamp) + str(method) + str(requestPath)\n        # Only add body if it exists and is not empty\n        # NOTE: Replace single quotes with double quotes(matching py-clob-client behavior)\n        # This is necessary to generate the same hmac message and typescript\n        messageWithBody = message\n        if body is not None and body != '':\n            messageWithBody = message + str(body).replace(\"'\", '\"')\n        # Generate HMAC and return URL-safe base64\n        # Convert URL-safe base64 to standard base64(replace - with + and _ with /)\n        secretBinary = self.base64_to_binary(str(secret).replace('-', '+').replace('_', '/'))\n        hmacResult = self.hmac(self.encode(messageWithBody), secretBinary, hashlib.sha256, 'base64')\n        return hmacResult.replace('+', '-').replace('/', '_')\n\n    def create_level2_headers(self, apiKey: str, timestamp: str, signature: str, password: str) -> dict:\n        \"\"\"\n        Creates Level 2 authentication headers\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/headers/headers.py\n\n        :param str apiKey: API key\n        :param str timestamp: timestamp string\n        :param str signature: signature string\n        :param str password: API passphrase\n        :returns dict: Level 2 headers dictionary\n        \"\"\"\n        authHeaders: dict = {\n            'POLY_API_KEY': apiKey,\n            'POLY_TIMESTAMP': timestamp,\n            'POLY_SIGNATURE': signature,\n            'POLY_PASSPHRASE': password,  # Passphrase is required for L2 authentication\n            'Content-Type': 'application/json',\n        }\n        # Always include POLY_ADDRESS in Level 2 headers(matches GitHub issue  #190 fix)\n        # Get wallet address from funder option, walletAddress property, or derive from privateKey\n        walletAddress = self.safe_string(self.options, 'funder')\n        if walletAddress is None and self.walletAddress is not None:\n            walletAddress = self.walletAddress\n        if walletAddress is None and self.privateKey is not None:\n            # Derive wallet address from private key if not provided\n            walletAddress = self.get_main_wallet_address()\n        if walletAddress is not None:\n            # Normalize and checksum the address(EIP-55)\n            walletAddress = self.normalize_address(walletAddress)\n            authHeaders['POLY_ADDRESS'] = walletAddress\n        #  # Add signature type if provided(defaults to EOA from options)\n        # signatureType = self.get_signature_type(params)\n        # eoaSignatureType = self.safe_integer(self.safe_dict(self.options, 'signatureTypes', {}), 'EOA', 0)\n        # if signatureType != eoaSignatureType:\n        #     authHeaders['POLY_SIGNATURE_TYPE'] = str(signatureType)\n        # }\n        #  # Add chain ID(defaults to 137 for Polygon mainnet, 80001 for testnet)\n        #  # chain_id: 137 = Polygon mainnet(default), 80001 = Polygon Mumbai testnet\n        # chainId = self.safe_integer(self.options, 'chainId', 137)\n        # authHeaders['POLY_CHAIN_ID'] = str(chainId)\n        return authHeaders\n\n    def build_private_request(self, baseUrl: str, pathWithParams: str, method: str, queryParams: dict, body: str = None, headers: dict = None) -> dict:\n        \"\"\"\n        Builds a private(authenticated) request with L2 authentication\n        :param str baseUrl: API base URL\n        :param str pathWithParams: path with parameters\n        :param str method: HTTP method\n        :param dict queryParams: query parameters\n        :param str [body]: request body\n        :param dict [headers]: existing headers\n        :returns dict: request object with url, method, body, and headers\n        \"\"\"\n        # Ensure privateKey is set\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' requires privateKey for authenticated requests')\n        # Get API credentials - self will raise if credentials not generated\n        # For lazy generation, ensureApiCredentials() should be called before self\n        creds = self.get_api_credentials()\n        timestamp = str(self.nonce())\n        # Serialize body deterministically if it's an object(matching py-clob-client)\n        # Use json.dumpswhich produces compact JSON by default(no spaces)\n        # This matches: json.dumps(body, separators=(\",\", \":\"), ensure_ascii=False)\n        serializedBody: str = None\n        if body is not None:\n            if isinstance(body, dict):\n                # Deterministic JSON: compact format(no spaces)\n                serializedBody = json.dumps(body)\n            else:\n                serializedBody = str(body)\n        elif queryParams and (method == 'POST' or method == 'PUT' or method == 'DELETE'):\n            # If body is None but we have queryParams for POST/PUT/DELETE, serialize them\n            serializedBody = json.dumps(queryParams)\n        # Build request path and payload using the serialized body\n        pathAndPayload = self.build_request_path_and_payload(pathWithParams, method, queryParams, serializedBody)\n        requestPath = pathAndPayload['requestPath']\n        requestUrl = pathAndPayload['url']\n        # Use the serialized body for the actual request(exact string that will be sent)\n        finalBody = serializedBody is not serializedBody if None else pathAndPayload['body']\n        privateUrl = baseUrl + requestUrl\n        # Create Level 2 signature: for GET requests, do NOT include query params in signature\n        # For POST/PUT/DELETE, include the serialized body(not query params)\n        # This matches py-clob-client: signature = timestamp + method + requestPath [+ body for non-GET]\n        bodyForSignature = None if (method == 'GET') else serializedBody\n        signature = self.create_level2_signature(timestamp, method, requestPath, bodyForSignature, creds['secret'])\n        # Create Level 2 headers\n        authHeaders = self.create_level2_headers(creds['apiKey'], timestamp, signature, creds['password'])\n        # Merge with existing headers\n        headers = self.build_default_headers(method, headers)\n        headers = self.extend(headers, authHeaders)\n        return {'url': privateUrl, 'method': method, 'body': finalBody, 'headers': headers}\n\n    def sign(self, path, api: Any = [ 'clob', 'public' ], method='GET', params={}, headers=None, body=None):\n        \"\"\"\n        Signs a request for authenticated endpoints\n\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param str path: API endpoint path\n        :param str api: API type('public' or 'private')\n        :param str method: HTTP method('GET', 'POST', etc.)\n        :param dict params: Request parameters\n        :param dict headers: Request headers\n        :param str body: Request body\n        :returns dict: Signed request with url, method, body, and headers\n        \"\"\"\n        # Get API base URL\n        baseUrl = self.get_api_base_url(params)\n        # Build path with parameters\n        pathWithParams = self.implode_params(path, params)\n        query = self.omit(params, self.extract_params(path))\n        # Remove api_type from query params's not part of the actual API request\n        queryParams = self.omit(query, ['api_type'])\n        # For public endpoints, no authentication needed\n        # api is always an array like ['gamma', 'public'] or ['clob', 'private']\n        # The second element is the access level(public/private)\n        accessLevel = self.safe_string(api, 1, 'public')\n        if accessLevel == 'public':\n            return self.build_public_request(baseUrl, pathWithParams, method, queryParams, body, headers)\n        # For private endpoints, use L2 authentication\n        return self.build_private_request(baseUrl, pathWithParams, method, queryParams, body, headers)\n\n    def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response: Any, requestHeaders: Any, requestBody: Any):\n        if response is None:\n            return None\n        # Polymarket API errors\n        if code >= 400:\n            # Explicitly check for 401(Unauthorized) and raise AuthenticationError\n            if code == 401:\n                authFeedback = self.id + ' ' + method + ' ' + url + ' 401 ' + reason + ' ' + body\n                raise AuthenticationError(authFeedback)\n            # Try to parse error message from response first(can be JSON or text)\n            # Check error message BEFORE status code to catch specific errors like \"Order not found\"\n            # that may return 400 status but should raise OrderNotFound instead of BadRequest\n            errorMessage = None\n            errorData = None\n            try:\n                if isinstance(response, str):\n                    errorMessage = response\n                elif isinstance(response, dict):\n                    errorMessage = self.safe_string(response, 'error')\n                    if errorMessage is None:\n                        errorMessage = self.safe_string(response, 'message')\n                    if errorMessage is None:\n                        # If no error/message field, use the whole response data\n                        errorData = response\n            except Exception as e:\n                errorMessage = body\n            feedback = self.id + ' ' + (errorMessage or body)\n            if errorMessage is not None:\n                # Try exact match first(e.g., \"Order not found\" -> OrderNotFound)\n                self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback)\n                # Then try broad match\n                self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback)\n                # If no match, fall through to status code check\n            # Check HTTP status code(use throwExactlyMatchedException for proper type handling)\n            # This handles cases where no specific error message is found in the response\n            codeAsString = str(code)\n            statusCodeFeedback = self.id + ' ' + method + ' ' + url + ' ' + codeAsString + ' ' + reason + ' ' + body\n            self.throw_exactly_matched_exception(self.exceptions['exact'], codeAsString, statusCodeFeedback)\n            # If we reach here, no exception was thrown, so raise a generic error\n            if errorData is not None:\n                raise ExchangeError(self.id + ' ' + self.json(errorData))\n            else:\n                raise ExchangeError(feedback)\n        return None\n"
  },
  {
    "path": "Trading/Exchange/polymarket/ccxt/polymarket_pro.py",
    "content": "# -*- coding: utf-8 -*-\n\n# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:\n# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code\n\nfrom .polymarket_async import polymarket\nfrom ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById\nfrom ccxt.base.types import Any, Int, Order, OrderBook, Str, Ticker, Trade\nfrom ccxt.async_support.base.ws.client import Client\nfrom typing import List\nfrom ccxt.base.errors import ArgumentsRequired\n\n\nclass polymarket(polymarket):\n\n    def describe(self) -> Any:\n        return self.deep_extend(super(polymarket, self).describe(), {\n            'has': {\n                'ws': True,\n                'watchBalance': False,\n                'watchTicker': True,\n                'watchTickers': False,\n                'watchTrades': True,\n                'watchTradesForSymbols': False,\n                'watchMyTrades': True,\n                'watchOrders': True,\n                'watchOrderBook': True,\n                'watchOHLCV': False,\n            },\n            'urls': {\n                'api': {\n                    'ws': {\n                        'market': 'wss://ws-subscriptions-clob.polymarket.com/ws/market',\n                        'user': 'wss://ws-subscriptions-clob.polymarket.com/ws/user',\n                        'liveData': 'wss://ws-live-data.polymarket.com',\n                    },\n                },\n            },\n            'options': {\n                'watchOrderBook': {\n                    'channel': 'book',\n                },\n            },\n            'streaming': {\n            },\n        })\n\n    async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:\n        \"\"\"\n        watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data\n        :param str symbol: unified symbol of the market to fetch the order book for\n        :param int [limit]: the maximum amount of order book entries to return\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_id]: the asset ID for the specific outcome(required if market has multiple outcomes)\n        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n        assetId = self.safe_string_2(params, 'asset_id', 'token_id')  # Support both for backward compatibility\n        # If asset_id not provided, use first token ID from market\n        if assetId is None:\n            if isinstance(clobTokenIds, list) and len(clobTokenIds) > 0:\n                assetId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' watchOrderBook() requires asset_id parameter when market has multiple outcomes')\n        url = self.urls['api']['ws']['market']\n        messageHash = 'orderbook:' + symbol + ':' + assetId\n        request: dict = {\n            'type': 'MARKET',\n            'assets_ids': [assetId],\n        }\n        subscription: dict = {\n            'symbol': symbol,\n            'asset_id': assetId,\n        }\n        orderbook = await self.watch(url, messageHash, request, messageHash, subscription)\n        return orderbook.limit(limit)\n\n    async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        get the list of most recent trades for a particular symbol\n        :param str symbol: unified symbol of the market to fetch trades for\n        :param int [since]: timestamp in ms of the earliest trade to fetch\n        :param int [limit]: the maximum amount of trades to fetch\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_id]: the asset ID for the specific outcome(required if market has multiple outcomes)\n        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n        assetId = self.safe_string_2(params, 'asset_id', 'token_id')  # Support both for backward compatibility\n        # If asset_id not provided, use first token ID from market\n        if assetId is None:\n            if isinstance(clobTokenIds, list) and len(clobTokenIds) > 0:\n                assetId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' watchTrades() requires asset_id parameter when market has multiple outcomes')\n        url = self.urls['api']['ws']['market']\n        messageHash = 'trades:' + symbol + ':' + assetId\n        request: dict = {\n            'type': 'MARKET',\n            'assets_ids': [assetId],\n        }\n        subscription: dict = {\n            'symbol': symbol,\n            'asset_id': assetId,\n        }\n        trades = await self.watch(url, messageHash, request, messageHash, subscription)\n        if self.newUpdates:\n            limit = trades.getLimit(symbol, limit)\n        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)\n\n    async def watch_ticker(self, symbol: str, params={}) -> Ticker:\n        \"\"\"\n        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market\n        :param str symbol: unified symbol of the market to fetch the ticker for\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_id]: the asset ID for the specific outcome(required if market has multiple outcomes)\n        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`\n        \"\"\"\n        await self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n        assetId = self.safe_string_2(params, 'asset_id', 'token_id')  # Support both for backward compatibility\n        # If asset_id not provided, use first token ID from market\n        if assetId is None:\n            if isinstance(clobTokenIds, list) and len(clobTokenIds) > 0:\n                assetId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' watchTicker() requires asset_id parameter when market has multiple outcomes')\n        url = self.urls['api']['ws']['market']\n        messageHash = 'ticker:' + symbol + ':' + assetId\n        request: dict = {\n            'type': 'MARKET',\n            'assets_ids': [assetId],\n        }\n        subscription: dict = {\n            'symbol': symbol,\n            'asset_id': assetId,\n        }\n        return await self.watch(url, messageHash, request, messageHash, subscription)\n\n    async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:\n        \"\"\"\n        watches information on an order made by the user\n        :param str [symbol]: unified symbol of the market the order was made in\n        :param int [since]: timestamp in ms of the earliest order to watch\n        :param int [limit]: the maximum amount of orders to watch\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        await self.authenticate(params)\n        messageHash = 'orders'\n        url = self.urls['api']['ws']['user']\n        request: dict = {\n            'type': 'USER',\n        }\n        if symbol is not None:\n            symbol = self.safe_symbol(symbol)\n            messageHash = messageHash + ':' + symbol\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            if conditionId is not None:\n                request['markets'] = [conditionId]\n        orders = await self.watch(url, messageHash, request, messageHash)\n        if self.newUpdates:\n            limit = orders.getLimit(symbol, limit)\n        return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)\n\n    async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        get the list of trades associated with the user\n        :param str [symbol]: unified symbol of the market to fetch trades for\n        :param int [since]: timestamp in ms of the earliest trade to fetch\n        :param int [limit]: the maximum amount of trades to fetch\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`\n        \"\"\"\n        await self.authenticate(params)\n        messageHash = 'myTrades'\n        url = self.urls['api']['ws']['user']\n        request: dict = {\n            'type': 'USER',\n        }\n        if symbol is not None:\n            symbol = self.safe_symbol(symbol)\n            messageHash = messageHash + ':' + symbol\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            if conditionId is not None:\n                request['markets'] = [conditionId]\n        trades = await self.watch(url, messageHash, request, messageHash)\n        if self.newUpdates:\n            limit = trades.getLimit(symbol, limit)\n        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)\n\n    def handle_order_book(self, client: Client, message):\n        #\n        # Market websocket order book event:\n        #     {\n        #         \"event_type\": \"book\",\n        #         \"asset_id\": \"0x...\",\n        #         \"bids\": [[price, size], ...],\n        #         \"asks\": [[price, size], ...],\n        #         \"timestamp\": 1234567890\n        #     }\n        #\n        # Or array of events:\n        #     [{...}, {...}]\n        #\n        messages = []\n        if isinstance(message, list):\n            messages = message\n        else:\n            messages = [message]\n        for i in range(0, len(messages)):\n            msg = messages[i]\n            eventType = self.safe_string(msg, 'event_type')\n            if eventType != 'book':\n                continue\n            assetId = self.safe_string(msg, 'asset_id')\n            # Find symbol and asset_id from subscriptions\n            symbol = None\n            subscriptionAssetId = None\n            subscriptionKeys = list(client.subscriptions.keys())\n            for j in range(0, len(subscriptionKeys)):\n                subscribeHash = subscriptionKeys[j]\n                subscription = client.subscriptions[subscribeHash]\n                if subscription != None:\n                    subAssetId = self.safe_string_2(subscription, 'asset_id', 'token_id')  # Support both for backward compatibility\n                    if subAssetId == assetId:\n                        symbol = self.safe_string(subscription, 'symbol')\n                        subscriptionAssetId = subAssetId\n                        break\n            if symbol is None:\n                # Try to resolve from asset_id\n                market = self.safe_market(assetId)\n                symbol = market['symbol']\n                subscriptionAssetId = assetId\n            messageHash = 'orderbook:' + symbol + ':' + subscriptionAssetId\n            if not (symbol in self.orderbooks):\n                self.orderbooks[symbol] = self.order_book({})\n            orderbook = self.orderbooks[symbol]\n            # Polymarket docs use `buys`/`sells` with OrderSummary objects, but some payloads use `bids`/`asks`\n            rawBids = self.safe_value_2(msg, 'bids', 'buys', [])\n            rawAsks = self.safe_value_2(msg, 'asks', 'sells', [])\n            bids = []\n            bidLevels = self.to_array(rawBids)\n            for j in range(0, len(bidLevels)):\n                level = bidLevels[j]\n                if isinstance(level, list):\n                    bids.append(level)\n                elif isinstance(level, dict):\n                    price = self.safe_string(level, 'price')\n                    size = self.safe_string(level, 'size')\n                    if price is not None and size is not None:\n                        bids.append([price, size])\n            asks = []\n            askLevels = self.to_array(rawAsks)\n            for j in range(0, len(askLevels)):\n                level = askLevels[j]\n                if isinstance(level, list):\n                    asks.append(level)\n                elif isinstance(level, dict):\n                    price = self.safe_string(level, 'price')\n                    size = self.safe_string(level, 'size')\n                    if price is not None and size is not None:\n                        asks.append([price, size])\n            rawTimestamp = self.safe_integer(msg, 'timestamp')\n            timestamp = None\n            if rawTimestamp is not None:\n                if rawTimestamp > 1000000000000:\n                    timestamp = rawTimestamp\n                else:\n                    timestamp = rawTimestamp * 1000\n            datetime = None\n            if timestamp is not None:\n                datetime = self.iso8601(timestamp)\n            snapshot = self.parse_order_book({'bids': bids, 'asks': asks}, symbol, timestamp)\n            orderbook.reset(snapshot)\n            orderbook['symbol'] = symbol\n            orderbook['timestamp'] = timestamp\n            orderbook['datetime'] = datetime\n            client.resolve(orderbook, messageHash)\n\n    def handle_trades(self, client: Client, message):\n        #\n        # Market websocket trade event:\n        #     {\n        #         \"event_type\": \"trade\",\n        #         \"asset_id\": \"0x...\",\n        #         \"trade_id\": \"0x...\",\n        #         \"price\": \"0.5\",\n        #         \"size\": \"100\",\n        #         \"side\": \"buy\",\n        #         \"timestamp\": 1234567890\n        #     }\n        #\n        messages = []\n        if isinstance(message, list):\n            messages = message\n        else:\n            messages = [message]\n        for i in range(0, len(messages)):\n            msg = messages[i]\n            eventType = self.safe_string(msg, 'event_type')\n            if eventType != 'trade':\n                continue\n            assetId = self.safe_string(msg, 'asset_id')\n            # Find symbol and asset_id from subscriptions\n            symbol = None\n            subscriptionAssetId = None\n            subscriptionKeys = list(client.subscriptions.keys())\n            for j in range(0, len(subscriptionKeys)):\n                subscribeHash = subscriptionKeys[j]\n                subscription = client.subscriptions[subscribeHash]\n                if isinstance(subscription, dict):\n                    subAssetId = self.safe_string_2(subscription, 'asset_id', 'token_id')  # Support both for backward compatibility\n                    if subAssetId == assetId:\n                        symbol = self.safe_string(subscription, 'symbol')\n                        subscriptionAssetId = subAssetId\n                        break\n            if symbol is None:\n                # Try to resolve from asset_id\n                market = self.safe_market(assetId)\n                symbol = market['symbol']\n                subscriptionAssetId = assetId\n            messageHash = 'trades:' + symbol + ':' + subscriptionAssetId\n            stored = self.safe_value(self.trades, symbol)\n            if stored is None:\n                limit = self.safe_integer(self.options, 'tradesLimit', 1000)\n                stored = ArrayCache(limit)\n                self.trades[symbol] = stored\n            market = self.market(symbol)\n            trade = self.parse_trade(msg, market)\n            # Normalize WS timestamp(Polymarket typically sends ms timestamps in WS payloads)\n            rawTimestamp = self.safe_integer(msg, 'timestamp')\n            wsTimestamp = None\n            if rawTimestamp is not None:\n                if rawTimestamp > 1000000000000:\n                    wsTimestamp = rawTimestamp\n                else:\n                    wsTimestamp = rawTimestamp * 1000\n            if wsTimestamp is not None:\n                trade['timestamp'] = wsTimestamp\n                trade['datetime'] = self.iso8601(wsTimestamp)\n            stored.append(trade)\n            client.resolve(stored, messageHash)\n\n    def handle_ticker(self, client: Client, message):\n        #\n        # Market websocket ticker events:\n        #     {\n        #         \"event_type\": \"price_change\",\n        #         \"asset_id\": \"0x...\",\n        #         \"price\": \"0.5\",\n        #         \"timestamp\": 1234567890\n        #     }\n        #     {\n        #         \"event_type\": \"last_trade_price\",\n        #         \"asset_id\": \"0x...\",\n        #         \"price\": \"0.5\",\n        #         \"timestamp\": 1234567890\n        #     }\n        #\n        messages = []\n        if isinstance(message, list):\n            messages = message\n        else:\n            messages = [message]\n        for i in range(0, len(messages)):\n            msg = messages[i]\n            eventType = self.safe_string(msg, 'event_type')\n            if eventType != 'price_change' and eventType != 'last_trade_price':\n                continue\n            # `last_trade_price` is per-asset, but `price_change` can be a batch containing `price_changes[]`.\n            # Docs: https://docs.polymarket.com/developers/CLOB/websocket/market-channel#price-change-message\n            rawTimestamp = self.safe_integer(msg, 'timestamp')\n            timestamp = None\n            if rawTimestamp is not None:\n                if rawTimestamp > 1000000000000:\n                    timestamp = rawTimestamp\n                else:\n                    timestamp = rawTimestamp * 1000\n            priceChanges = self.safe_value(msg, 'price_changes')\n            updates: List[Any] = []\n            if eventType == 'price_change' and isinstance(priceChanges, list):\n                updates = priceChanges\n            else:\n                updates = [msg]\n            for k in range(0, len(updates)):\n                update = updates[k]\n                assetId = self.safe_string(update, 'asset_id', self.safe_string(msg, 'asset_id'))\n                if assetId is None:\n                    continue\n                # Find symbol and asset_id from subscriptions\n                symbol = None\n                subscriptionAssetId = None\n                subscriptionKeys = list(client.subscriptions.keys())\n                for j in range(0, len(subscriptionKeys)):\n                    subscribeHash = subscriptionKeys[j]\n                    subscription = client.subscriptions[subscribeHash]\n                    if isinstance(subscription, dict):\n                        subAssetId = self.safe_string_2(subscription, 'asset_id', 'token_id')  # Support both for backward compatibility\n                        if subAssetId == assetId:\n                            symbol = self.safe_string(subscription, 'symbol')\n                            subscriptionAssetId = subAssetId\n                            break\n                if symbol is None:\n                    # Try to resolve from asset_id\n                    market = self.safe_market(assetId)\n                    symbol = market['symbol']\n                    subscriptionAssetId = assetId\n                messageHash = 'ticker:' + symbol + ':' + subscriptionAssetId\n                market = self.market(symbol)\n                prev = self.safe_value(self.tickers, symbol, {})\n                last = self.safe_number(update, 'price', self.safe_number(msg, 'price', self.safe_number(prev, 'last')))\n                bid = self.safe_number(update, 'best_bid', self.safe_number(prev, 'bid', last))\n                ask = self.safe_number(update, 'best_ask', self.safe_number(prev, 'ask', last))\n                info = msg\n                if eventType == 'price_change':\n                    info = update\n                datetime = None\n                if timestamp is not None:\n                    datetime = self.iso8601(timestamp)\n                ticker: Ticker = {\n                    'symbol': symbol,\n                    'info': info,\n                    'timestamp': timestamp,\n                    'datetime': datetime,\n                    'last': last,\n                    'bid': bid,\n                    'bidVolume': None,\n                    'ask': ask,\n                    'askVolume': None,\n                    'high': None,\n                    'low': None,\n                    'open': None,\n                    'close': last,\n                    'previousClose': None,\n                    'change': None,\n                    'percentage': None,\n                    'average': None,\n                    'baseVolume': None,\n                    'quoteVolume': None,\n                    'vwap': None,\n                    'indexPrice': None,\n                    'markPrice': None,\n                }\n                self.tickers[symbol] = ticker\n                client.resolve(ticker, messageHash)\n\n    def handle_orders(self, client: Client, message):\n        #\n        # User websocket order event:\n        #     {\n        #         \"event_type\": \"order\",\n        #         \"order_id\": \"0x...\",\n        #         \"asset_id\": \"0x...\",\n        #         \"side\": \"buy\",\n        #         \"price\": \"0.5\",\n        #         \"size\": \"100\",\n        #         \"status\": \"open\",\n        #         \"timestamp\": 1234567890\n        #     }\n        #\n        eventType = self.safe_string(message, 'event_type')\n        if eventType != 'order':\n            return\n        messageHash = 'orders'\n        stored = self.orders\n        if stored is None:\n            limit = self.safe_integer(self.options, 'ordersLimit', 1000)\n            stored = ArrayCacheBySymbolById(limit)\n            self.orders = stored\n        order = self.parse_order(message)\n        rawTimestamp = self.safe_integer(message, 'timestamp')\n        wsTimestamp = None\n        if rawTimestamp is not None:\n            if rawTimestamp > 1000000000000:\n                wsTimestamp = rawTimestamp\n            else:\n                wsTimestamp = rawTimestamp * 1000\n        if wsTimestamp is not None:\n            order['timestamp'] = wsTimestamp\n            order['datetime'] = self.iso8601(wsTimestamp)\n        orderSymbols: dict = {}\n        orderSymbols[order['symbol']] = True\n        stored.append(order)\n        unique = list(orderSymbols.keys())\n        for i in range(0, len(unique)):\n            symbol = unique[i]\n            symbolSpecificMessageHash = messageHash + ':' + symbol\n            client.resolve(stored, symbolSpecificMessageHash)\n        client.resolve(stored, messageHash)\n\n    def handle_my_trades(self, client: Client, message):\n        #\n        # User websocket trade event:\n        #     {\n        #         \"event_type\": \"trade\",\n        #         \"trade_id\": \"0x...\",\n        #         \"asset_id\": \"0x...\",\n        #         \"side\": \"buy\",\n        #         \"price\": \"0.5\",\n        #         \"size\": \"100\",\n        #         \"timestamp\": 1234567890\n        #     }\n        #\n        eventType = self.safe_string(message, 'event_type')\n        if eventType != 'trade':\n            return\n        messageHash = 'myTrades'\n        stored = self.myTrades\n        if stored is None:\n            limit = self.safe_integer(self.options, 'tradesLimit', 1000)\n            stored = ArrayCacheBySymbolById(limit)\n            self.myTrades = stored\n        trade = self.parse_trade(message)\n        rawTimestamp = self.safe_integer(message, 'timestamp')\n        wsTimestamp = None\n        if rawTimestamp is not None:\n            if rawTimestamp > 1000000000000:\n                wsTimestamp = rawTimestamp\n            else:\n                wsTimestamp = rawTimestamp * 1000\n        if wsTimestamp is not None:\n            trade['timestamp'] = wsTimestamp\n            trade['datetime'] = self.iso8601(wsTimestamp)\n        tradeSymbols: dict = {}\n        tradeSymbols[trade['symbol']] = True\n        stored.append(trade)\n        unique = list(tradeSymbols.keys())\n        uniqueLength = len(unique)\n        if uniqueLength == 0:\n            return\n        for i in range(0, len(unique)):\n            symbol = unique[i]\n            symbolSpecificMessageHash = messageHash + ':' + symbol\n            client.resolve(stored, symbolSpecificMessageHash)\n        client.resolve(stored, messageHash)\n\n    def handle_message(self, client: Client, message):\n        #\n        # Market websocket messages can be:\n        #     - Single event object: {\"event_type\": \"book\", ...}\n        #     - Array of events: [{\"event_type\": \"book\", ...}, ...]\n        #     - Ready event: {\"event\": \"ready\"} or similar(check Python code)\n        #\n        # User websocket messages:\n        #     - Single event object: {\"event_type\": \"order\", ...}\n        #\n        # Check for ready event first(Polymarket may send self)\n        event = self.safe_string(message, 'event')\n        if event == 'ready' or event == 'connected':\n            # Connection ready - subscriptions are sent automatically by base watch() method\n            return\n        if isinstance(message, list):\n            # Handle array of events(market websocket)\n            self.handle_market_events(client, message)\n        else:\n            eventType = self.safe_string(message, 'event_type')\n            url = client.url\n            # Determine which websocket based on URL\n            if url.find('/ws/market') >= 0:\n                # Market websocket\n                self.handle_market_event(client, message, eventType)\n            elif url.find('/ws/user') >= 0:\n                # User websocket\n                self.handle_user_event(client, message, eventType)\n            elif url.find('ws-live-data') >= 0:\n                # Live data websocket - not implemented yet\n                if self.verbose:\n                    self.log('Live data websocket message:', message)\n\n    def handle_market_events(self, client: Client, messages: List[Any]):\n        # Handle array of market events\n        for i in range(0, len(messages)):\n            msg = messages[i]\n            eventType = self.safe_string(msg, 'event_type')\n            self.handle_market_event(client, msg, eventType)\n\n    def handle_market_event(self, client: Client, message: Any, eventType: str):\n        if eventType == 'book':\n            self.handle_order_book(client, message)\n        elif eventType == 'trade':\n            self.handle_trades(client, message)\n        elif eventType == 'price_change' or eventType == 'last_trade_price':\n            self.handle_ticker(client, message)\n        elif eventType == 'tick_size_change':\n            # Tick size change - can be used to update ticker\n            if self.verbose:\n                self.log('Tick size change event:', message)\n        else:\n            # Unknown event type, log but don't error\n            if self.verbose:\n                self.log('Unknown market websocket event type:', eventType, message)\n\n    def handle_user_event(self, client: Client, message: Any, eventType: str):\n        if eventType == 'order':\n            self.handle_orders(client, message)\n        elif eventType == 'trade':\n            self.handle_my_trades(client, message)\n        else:\n            # Unknown event type, log but don't error\n            if self.verbose:\n                self.log('Unknown user websocket event type:', eventType, message)\n\n    async def authenticate(self, params={}):\n        url = self.urls['api']['ws']['user']\n        client = self.client(url)\n        messageHash = 'authenticated'\n        future = self.safe_value(client.subscriptions, messageHash)\n        if future is None:\n            # Get API credentials\n            creds = await self.ensureApiCredentials(params)\n            # Build auth payload matching Python implementation\n            # auth=creds.model_dump(by_alias=True) in Python becomes:\n            auth: dict = {\n                'apiKey': creds['apiKey'],\n                'secret': creds['secret'],\n                'passphrase': creds['passphrase'],\n            }\n            request: dict = {\n                'auth': auth,\n                'type': 'USER',\n            }\n            future = await self.watch(url, messageHash, request, messageHash)\n            client.subscriptions[messageHash] = future\n        return future\n\n    async def watch(self, url: str, messageHash: str, message=None, subscribeHash=None, subscription=None):\n        client = self.client(url)\n        if subscribeHash is None:\n            subscribeHash = messageHash\n        # Store subscription info for market websocket to use in handleMessage\n        if subscription is not None and url.find('/ws/market') >= 0:\n            # Store subscription separately so we can look it up by asset_id\n            if not (subscribeHash in client.subscriptions):\n                client.subscriptions[subscribeHash] = subscription\n        return await super(polymarket, self).watch(url, messageHash, message, subscribeHash, subscription)\n\n    def on_connected(self, client: Client):\n        # Called when websocket connection is established\n        # The base watch() method will send the message automatically\n        # But for Polymarket, we may need to wait for a \"ready\" event\n        # For now, the base class handle it\n        super(polymarket, self).on_connected(client)\n"
  },
  {
    "path": "Trading/Exchange/polymarket/ccxt/polymarket_sync.py",
    "content": "# -*- coding: utf-8 -*-\n\n# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:\n# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code\n\nfrom ccxt.base.exchange import Exchange\nfrom .polymarket_abstract import ImplicitAPI\nimport hashlib\nimport math\nimport json\nimport numbers\nfrom ccxt.base.types import Any, Int, Market, MarketType, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFeeInterface\nfrom typing import List\nfrom ccxt.base.errors import ExchangeError\nfrom ccxt.base.errors import AuthenticationError\nfrom ccxt.base.errors import PermissionDenied\nfrom ccxt.base.errors import ArgumentsRequired\nfrom ccxt.base.errors import BadRequest\nfrom ccxt.base.errors import InsufficientFunds\nfrom ccxt.base.errors import InvalidOrder\nfrom ccxt.base.errors import OrderNotFound\nfrom ccxt.base.errors import NetworkError\nfrom ccxt.base.errors import RateLimitExceeded\nfrom ccxt.base.errors import ExchangeNotAvailable\nfrom ccxt.base.errors import OnMaintenance\nfrom ccxt.base.decimal_to_precision import ROUND\nfrom ccxt.base.decimal_to_precision import TICK_SIZE\nfrom ccxt.base.precise import Precise\n\n\nclass polymarket(Exchange, ImplicitAPI):\n\n    def describe(self) -> Any:\n        return self.deep_extend(super(polymarket, self).describe(), {\n            'id': 'polymarket',\n            'name': 'Polymarket',\n            'countries': ['US'],\n            'version': '1',\n            # Rate limits are enforced using Cloudflare's throttling system\n            # Requests over the limit are throttled/delayed rather than rejected\n            # See https://docs.polymarket.com/quickstart/introduction/rate-limits\n            # Cost calculation formula: cost = (1000 / rateLimit) * 60 / requests_per_minute\n            # With rateLimit = 50ms(20 req/s = 1200 req/min), base cost = 1.0\n            # General limits:\n            # - General Rate Limiting: 5000 requests / 10s(500 req/s = 30000 req/min) => cost = 0.04\n            # - CLOB(General): 5000 requests / 10s(500 req/s = 30000 req/min) => cost = 0.04\n            # - GAMMA(General): 750 requests / 10s(75 req/s = 4500 req/min) => cost = 0.267\n            # - Data API(General): 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n            # Setting to 50ms(20 req/s) to match the most restrictive general limit(Data API)\n            # Specific endpoint costs are calculated relative to self base rateLimit\n            'rateLimit': 50,  # 20 requests per second(matches Data API general limit)\n            'certified': False,\n            'pro': True,\n            'requiredCredentials': {\n                'apiKey': False,\n                'secret': False,\n                'walletAddress': True,\n                'privateKey': True,\n            },\n            'has': {\n                'CORS': None,\n                'spot': False,\n                'margin': False,\n                'swap': False,\n                'future': False,\n                'option': True,\n                'addMargin': False,\n                'cancelOrder': True,\n                'cancelOrders': True,\n                'createDepositAddress': True,  # TODO with https://docs.polymarket.com/developers/misc-endpoints/bridge-deposit\n                'createMarketBuyOrderWithCost': False,\n                'createMarketOrder': True,\n                'createMarketOrderWithCost': False,\n                'createMarketSellOrderWithCost': False,\n                'createOrder': True,\n                'createOrders': True,\n                'createStopLimitOrder': False,\n                'createStopMarketOrder': False,\n                'createStopOrder': False,\n                'editOrder': False,\n                'fetchBalance': True,\n                'fetchBorrowInterest': False,\n                'fetchBorrowRateHistories': False,\n                'fetchBorrowRateHistory': False,\n                'fetchClosedOrders': False,\n                'fetchCrossBorrowRate': False,\n                'fetchCrossBorrowRates': False,\n                'fetchCurrencies': False,\n                'fetchDepositAddress': False, \n                'fetchDepositAddresses': True,  # TODO with https://docs.polymarket.com/developers/misc-endpoints/bridge-supported-assets\n                'fetchDepositAddressesByNetwork': True,  # TODO with https://docs.polymarket.com/developers/misc-endpoints/bridge-supported-assets\n                'fetchDeposits': False,\n                'fetchFundingHistory': False,\n                'fetchFundingRate': False,\n                'fetchFundingRateHistory': False,\n                'fetchFundingRates': False,\n                'fetchIndexOHLCV': False,\n                'fetchIsolatedBorrowRate': False,\n                'fetchIsolatedBorrowRates': False,\n                'fetchLedger': False,\n                'fetchLedgerEntry': False,\n                'fetchLeverageTiers': False,\n                'fetchMarkets': True,\n                'fetchMarkOHLCV': False,\n                'fetchMyTrades': True,\n                'fetchOHLCV': True,\n                'fetchOpenInterest': True,\n                'fetchOpenInterestHistory': False,\n                'fetchOpenOrders': True,\n                'fetchOrder': True,\n                'fetchOrderBook': True,\n                'fetchOrderBooks': True,\n                'fetchOrders': True,\n                'fetchPositionMode': False,\n                'fetchPremiumIndexOHLCV': False,\n                'fetchStatus': True,\n                'fetchTicker': True,\n                'fetchTickers': True,\n                'fetchTime': True,\n                'fetchTrades': True,\n                'fetchTradingFee': True,\n                'fetchTradingFees': False,\n                'fetchWithdrawals': False,\n                'setLeverage': False,\n                'setMarginMode': False,\n                'transfer': False,\n                'withdraw': False,\n            },\n            'urls': {\n                'logo': 'https://polymarket.com/favicon.ico',\n                'api': {\n                    'gamma': 'https://gamma-api.polymarket.com',\n                    'clob': 'https://clob.polymarket.com',  # Can be overridden with options.clobHost\n                    'data': 'https://data-api.polymarket.com',\n                    'bridge': 'https://bridge.polymarket.com',\n                    'ws': 'wss://ws-subscriptions-clob.polymarket.com/ws/',  # CLOB WebSocket for subscriptions\n                    'rtds': 'wss://ws-live-data.polymarket.com',  # Real Time Data Socket for crypto prices and comments\n                },\n                'test': {},  # TODO if exists\n                'www': 'https://polymarket.com',\n                'doc': [\n                    'https://docs.polymarket.com',\n                ],\n                'fees': 'https://docs.polymarket.com/developers/CLOB/introduction',\n            },\n            'api': {\n                # GAMMA API: https://gamma-api.polymarket.com\n                # Rate limits: https://docs.polymarket.com/quickstart/introduction/rate-limits\n                # Cost calculation: cost = (1000 / 50) * 60 / requests_per_minute = 1200 / requests_per_minute\n                # - GAMMA(General): 750 requests / 10s(75 req/s = 4500 req/min) => cost = 0.267\n                # - GAMMA Get Comments: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA /events: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA /markets: 125 requests / 10s(12.5 req/s = 750 req/min) => cost = 1.6\n                # - GAMMA /markets /events listing: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA Tags: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - GAMMA Search: 300 requests / 10s(30 req/s = 1800 req/min) => cost = 0.667\n                'gamma': {\n                    'public': {\n                        'get': {\n                            # Market endpoints\n                            'markets': 1.6,                     # GET /markets - used by fetchMarkets(125 req/10s = 750 req/min)\n                            'markets/{id}': 0.267,              # GET /markets/{id} - used by gammaPublicGetMarketsId(general limit)\n                            'markets/{id}/tags': 2.0,            # GET /markets/{id}/tags - used by gammaPublicGetMarketsIdTags(100 req/10s = 600 req/min)\n                            'markets/slug/{slug}': 0.267,        # GET /markets/slug/{slug} - used by gammaPublicGetMarketsSlugSlug(general limit)\n                            # Event endpoints\n                            'events': 2.0,                      # GET /events - used by gammaPublicGetEvents(100 req/10s = 600 req/min)\n                            'events/{id}': 0.267,                # GET /events/{id} - used by gammaPublicGetEventsId(general limit)\n                            # Series endpoints\n                            'series': 0.267,                     # GET /series - used by gammaPublicGetSeries(general limit)\n                            'series/{id}': 0.267,               # GET /series/{id} - used by gammaPublicGetSeriesId(general limit)\n                            # Search endpoints\n                            'search': 0.667,                     # GET /search - used by gammaPublicGetSearch(300 req/10s = 1800 req/min)\n                            # Comment endpoints\n                            'comments': 2.0,                     # GET /comments - used by gammaPublicGetComments(100 req/10s = 600 req/min)\n                            'comments/{id}': 0.267,             # GET /comments/{id} - used by gammaPublicGetCommentsId(general limit)\n                            # Sports endpoints\n                            'sports': 0.267,                    # GET /sports - used by gammaPublicGetSports(general limit)\n                            'sports/{id}': 0.267,               # GET /sports/{id} - used by gammaPublicGetSportsId(general limit)\n                        },\n                    },\n                },\n                # Data-API: https://data-api.polymarket.com\n                # Rate limits: https://docs.polymarket.com/quickstart/introduction/rate-limits\n                # Cost calculation: cost = (1000 / 50) * 60 / requests_per_minute = 1200 / requests_per_minute\n                # - Data API(General): 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - Data API(Alternative): 1200 requests / 1 minute(20 req/s = 1200 req/min) => cost = 1.0\n                # - Data API /trades: 75 requests / 10s(7.5 req/s = 450 req/min) => cost = 2.67\n                # - Data API \"OK\" Endpoint: 10 requests / 10s(1 req/s = 60 req/min) => cost = 20.0\n                'data': {\n                    'public': {\n                        'get': {\n                            # Core endpoints(from Data-API)\n                            'positions': 1.0,                     # GET /positions - used by dataPublicGetPositions(200 req/10s = 1200 req/min)\n                            'trades': 2.67,                      # GET /trades - used by dataPublicGetTrades(75 req/10s = 450 req/min)\n                            'activity': 1.0,                      # GET /activity - used by dataPublicGetActivity(200 req/10s = 1200 req/min)\n                            'holders': 1.0,                       # GET /holders - used by dataPublicGetHolders(200 req/10s = 1200 req/min)\n                            'value': 1.0,                         # GET /value - used by dataPublicGetTotalValue(200 req/10s = 1200 req/min)\n                            'closed-positions': 1.0,             # GET /closed-positions - used by dataPublicGetClosedPositions(200 req/10s = 1200 req/min)\n                            # Misc endpoints(from Data-API)\n                            'traded': 1.0,                        # GET /traded - used by dataPublicGetTraded(200 req/10s = 1200 req/min)\n                            'oi': 1.0,                            # GET /oi - used by dataPublicGetOpenInterest(200 req/10s = 1200 req/min)\n                            'live-volume': 1.0,                   # GET /live-volume - used by dataPublicGetLiveVolume(200 req/10s = 1200 req/min)\n                        },\n                    },\n                },\n                # Bridge API: https://bridge.polymarket.com\n                # Rate limits: Not explicitly documented, using conservative general rate limits\n                # Assuming similar to Data API: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                'bridge': {\n                    'public': {\n                        'get': {\n                            # Bridge endpoints\n                            'supported-assets': 1.0,              # GET /supported-assets - used by bridgePublicGetSupportedAssets(assumed 200 req/10s)\n                        },\n                        'post': {\n                            # Bridge endpoints\n                            'deposit': 1.0,                       # POST /deposit - used by bridgePublicPostDeposit(assumed 200 req/10s)\n                        },\n                    },\n                },\n                # CLOB API: https://clob.polymarket.com\n                # Rate limits: https://docs.polymarket.com/quickstart/introduction/rate-limits\n                # Cost calculation: cost = (1000 / 50) * 60 / requests_per_minute = 1200 / requests_per_minute\n                # General CLOB Endpoints:\n                # - CLOB(General): 5000 requests / 10s(500 req/s = 30000 req/min) => cost = 0.04\n                # - CLOB GET Balance Allowance: 125 requests / 10s(12.5 req/s = 750 req/min) => cost = 1.6\n                # - CLOB UPDATE Balance Allowance: 20 requests / 10s(2 req/s = 120 req/min) => cost = 10.0\n                # CLOB Market Data:\n                # - CLOB /book: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB /books: 80 requests / 10s(8 req/s = 480 req/min) => cost = 2.5\n                # - CLOB /price: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB /prices: 80 requests / 10s(8 req/s = 480 req/min) => cost = 2.5\n                # - CLOB /midprice: 200 requests / 10s(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB /midprices: 80 requests / 10s(8 req/s = 480 req/min) => cost = 2.5\n                # CLOB Ledger Endpoints:\n                # - CLOB Ledger(/trades /orders /notifications /order): 300 requests / 10s(30 req/s = 1800 req/min) => cost = 0.667\n                # - CLOB Ledger /data/orders: 150 requests / 10s(15 req/s = 900 req/min) => cost = 1.33\n                # - CLOB Ledger /data/trades: 150 requests / 10s(15 req/s = 900 req/min) => cost = 1.33\n                # - CLOB /notifications: 125 requests / 10s(12.5 req/s = 750 req/min) => cost = 1.6\n                # CLOB Markets & Pricing:\n                # - CLOB Price History: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # - CLOB Markets: 250 requests / 10s(25 req/s = 1500 req/min) => cost = 0.8\n                # - CLOB Market Tick Size: 50 requests / 10s(5 req/s = 300 req/min) => cost = 4.0\n                # - CLOB markets/0x: 50 requests / 10s(5 req/s = 300 req/min) => cost = 4.0\n                # - CLOB /markets listing: 100 requests / 10s(10 req/s = 600 req/min) => cost = 2.0\n                # CLOB Authentication:\n                # - CLOB API Keys: 50 requests / 10s(5 req/s = 300 req/min) => cost = 4.0\n                # CLOB Trading Endpoints(using sustained limits, not BURST):\n                # - CLOB POST /order: 24000 requests / 10 minutes(40 req/s = 2400 req/min) => cost = 0.5\n                # - CLOB DELETE /order: 24000 requests / 10 minutes(40 req/s = 2400 req/min) => cost = 0.5\n                # - CLOB POST /orders: 12000 requests / 10 minutes(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB DELETE /orders: 12000 requests / 10 minutes(20 req/s = 1200 req/min) => cost = 1.0\n                # - CLOB DELETE /cancel-all: 3000 requests / 10 minutes(5 req/s = 300 req/min) => cost = 4.0\n                # - CLOB DELETE /cancel-market-orders: 12000 requests / 10 minutes(20 req/s = 1200 req/min) => cost = 1.0\n                'clob': {\n                    'public': {\n                        'get': {\n                            # Order book endpoints\n                            'orderbook': 1.0,                     # GET /book - used by fetchOrderBook(200 req/10s = 1200 req/min)\n                            'orderbook/{token_id}': 0.04,        # Not used(deprecated format, general limit)\n                            # Trade endpoints\n                            'market/{condition_id}/trades': 0.04,  # Not used(deprecated, use /trades instead, general limit)\n                            'trades': 0.667,                    # GET /data/trades - used by fetchTrades(300 req/10s = 1800 req/min)\n                            # Price history endpoints\n                            'prices-history': 2.0,              # GET /prices-history - used by fetchOHLCV(100 req/10s = 600 req/min)\n                            # Pricing endpoints\n                            'price': 1.0,                       # GET /price - available but using POST /prices instead(200 req/10s = 1200 req/min)\n                            'prices': 2.5,                      # GET /prices - used by fetchTickers(80 req/10s = 480 req/min)\n                            # Midpoint endpoints\n                            'midpoint': 1.0,                    # GET /midpoint - used by fetchTicker(200 req/10s = 1200 req/min)\n                            'midpoints': 2.5,                   # GET /midpoints - available for fetchTickers enhancement(80 req/10s = 480 req/min)\n                            # Spread endpoints\n                            'spread': 0.04,                     # GET /spread - available for fetchTicker enhancement(general limit)\n                            # Last trade price endpoints\n                            'last-trade-price': 0.04,           # GET /last-trade-price - available for ticker enhancement(general limit)\n                            'last-trades-prices': 0.04,         # GET /last-trades-prices - available for tickers enhancement(general limit)\n                            # Utility endpoints\n                            '': 4.0,                            # GET / - health check endpoint used by fetchStatus/clobPublicGetOk(50 req/10s = 300 req/min)\n                            'time': 0.04,                       # GET /time - used by fetchTime(general limit)\n                            'tick-size': 4.0,                   # GET /tick-size - used for market precision(50 req/10s = 300 req/min)\n                            'neg-risk': 0.04,                   # GET /neg-risk - used for market metadata(general limit)\n                            'fee-rate': 0.04,                   # GET /fee-rate - used by fetchTradingFee(general limit)\n                            'markets': 2.0,                     # GET /markets - used by fetchMarkets(100 req/10s = 600 req/min)\n                        },\n                        'post': {\n                            # Order book endpoints\n                            'books': 2.5,                      # POST /books - used by fetchOrderBooks(80 req/10s = 480 req/min)\n                            # Spread endpoints\n                            'spreads': 0.04,                    # POST /spreads - used by fetchTickers(optional, general limit)\n                            # Pricing endpoints\n                            'prices': 2.5,                      # POST /prices - used by fetchTicker(80 req/10s = 480 req/min)\n                        },\n                    },\n                    'private': {\n                        'get': {\n                            # Order endpoints\n                            'order': 0.667,                     # GET /data/order/{order_id} - used by fetchOrder(300 req/10s = 1800 req/min)\n                            'orders': 1.33,                     # GET /data/orders - used by fetchOrders, fetchOpenOrders(150 req/10s = 900 req/min)\n                            # Trade endpoints\n                            'trades': 0.667,                    # GET /data/trades - used by fetchMyTrades(300 req/10s = 1800 req/min)\n                            'builder-trades': 0.667,             # GET /builder-trades - used for builder trades(300 req/10s = 1800 req/min)\n                            # Notification endpoints\n                            'notifications': 1.6,                # GET /notifications - used by getNotifications(125 req/10s = 750 req/min)\n                            # Balance endpoints\n                            'balance-allowance': 1.6,           # GET /balance-allowance - used by fetchBalance/getBalanceAllowance(125 req/10s = 750 req/min)\n                            # Order scoring endpoints\n                            'order-scoring': 0.04,               # GET /order-scoring - used by isOrderScoring(general limit)\n                            # API credential endpoints(L1 authentication - uses manual URL building)\n                            'auth/derive-api-key': 4.0,         # GET /auth/derive-api-key - used by derive_api_key(50 req/10s = 300 req/min)\n                        },\n                        'post': {\n                            # Order creation endpoints\n                            'order': 0.5,                       # POST /order - used by createOrder(24000 req/10min = 2400 req/min sustained)\n                            'orders': 1.0,                      # POST /orders - used by createOrders(12000 req/10min = 1200 req/min sustained)\n                            # Order scoring endpoints\n                            'orders-scoring': 0.04,             # POST /orders-scoring - used by areOrdersScoring(general limit)\n                            # API credential endpoints\n                            'auth/api-key': 4.0,                # POST /auth/api-key - used by create_or_derive_api_creds(50 req/10s = 300 req/min)\n                        },\n                        'delete': {\n                            # Order cancellation endpoints\n                            'order': 0.5,                       # DELETE /order - used by cancelOrder(24000 req/10min = 2400 req/min sustained)\n                            'orders': 1.0,                      # DELETE /orders - used by cancelOrders(12000 req/10min = 1200 req/min sustained)\n                            'cancel-all': 4.0,                   # DELETE /cancel-all - used by cancelAllOrders(3000 req/10min = 300 req/min sustained)\n                            'cancel-market-orders': 1.0,        # DELETE /cancel-market-orders - used for canceling market orders(12000 req/10min = 1200 req/min sustained)\n                            # Notification endpoints\n                            'notifications': 0.04,               # DELETE /notifications - used by dropNotifications(general limit)\n                        },\n                        'put': {\n                            # Balance endpoints\n                            'balance-allowance': 10.0,           # PUT /balance-allowance - used by updateBalanceAllowance(20 req/10s = 120 req/min)\n                        },\n                    },\n                },\n            },\n            'timeframes': {\n                '1m': '1m',\n                '1h': '1h',\n                '6h': '6h',\n                '1d': '1d',\n                '1w': '1w',\n            },\n            'fees': {\n                'trading': {\n                    'tierBased': False,\n                    'percentage': True,\n                    'taker': self.parse_number('0.02'),  # 2% taker fee(approximate)\n                    'maker': self.parse_number('0.02'),  # 2% maker fee(approximate)\n                },\n            },\n            'options': {\n                'fetchMarkets': {\n                    'active': True,  # only fetch active markets by default\n                    'closed': False,\n                    'archived': False,\n                },\n                'funder': None,  # Address that holds funds(walletAddress, required for proxy wallets like email/Magic wallets)\n                'proxyWallet': None,  # Proxy wallet address for Data-API endpoints(defaults to funder/walletAddress if not set)\n                'builderWallet': None,  # Builder wallet address(defaults to funder/walletAddress if not set)\n                'signatureTypes': {\n                    # https://docs.polymarket.com/developers/CLOB/orders/orders#signature-types\n                    'EOA': 0,  # EIP712 signature signed by an EOA\n                    'POLY_PROXY': 1,  # EIP712 signatures signed by a signer associated with funding Polymarket proxy wallet\n                    'POLY_GNOSIS_SAFE': 2,  # EIP712 signatures signed by a signer associated with funding Polymarket gnosis safe wallet\n                },\n                'side': None,  # Order side: 'BUY' or 'SELL'(default: None, must be provided)\n                'sides': {\n                    'BUY': 0,  # Buy side(maker gives USDC, wants tokens)\n                    'SELL': 1,  # Sell side(maker gives tokens, wants USDC)\n                },\n                'chainId': 137,  # Chain ID: 137 = Polygon mainnet(default), 80001 = Polygon Mumbai testnet\n                'chainName': 'polygon-mainnet',  # Chain name: 'polygon-mainnet'(default), 'polygon-mumbai'(testnet)\n                'sandboxMode': False,  # Enable sandbox/testnet mode(uses Polygon Mumbai testnet)\n                'clobHost': None,  # Custom CLOB API endpoint(defaults to https://clob.polymarket.com)\n                'defaultCollateral': 'USDC',  # Default collateral currency\n                'defaultExpirationDays': 30,  # Default expiration in days(default: 30 days from now)\n                'defaultFeeRateBps': 200,  # Default fee rate fallback in basis points(default: 200 bps = 2%)\n                'defaultTickSize': '0.01',  # Default tick size for rounding config(default: 0.01 = 2 decimal places for price, 2 for size, 4 for amount)\n                'marketOrderQuoteDecimals': 2,  # Max decimal places for quote currency(USDC) in market orders(default: 2)\n                'marketOrderBaseDecimals': 4,  # Max decimal places for base currency(tokens) in market orders(default: 4)\n                'roundingBufferDecimals': 4,  # Additional decimal places buffer for rounding up before final rounding down(default: 4)\n                # Constants matching clob-client\n                # See https://github.com/Polymarket/clob-client/blob/main/src/signing/constants.ts\n                # See https://github.com/Polymarket/clob-client/blob/main/src/constants.ts\n                'clobDomainName': 'ClobAuthDomain',\n                'clobVersion': '1',\n                'msgToSign': 'This message attests that I control the given wallet',\n                'initialCursor': 'MA==',  # Base64 encoded empty string, matches clob-client INITIAL_CURSOR\n                'endCursor': 'LTE=',  # Sentinel value indicating end of pagination\n                'defaultTokenId': None,  # Default token ID for conditional tokens\n                # Constants matching py-clob-client\n                # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/constants.py\n                'zeroAddress': '0x0000000000000000000000000000000000000000',  # Zero address for open orders(taker)\n                # EIP-712 domain constants matching clob-order-utils\n                # See https://github.com/Polymarket/clob-order-utils/blob/main/src/exchange.order.const.ts\n                'orderDomainName': 'Polymarket CTF Exchange',  # EIP-712 domain name for orders(PROTOCOL_NAME)\n                'orderDomainVersion': '1',  # EIP-712 domain version for orders(PROTOCOL_VERSION)\n                # Contract addresses for all networks\n                # See https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n                'contracts': {\n                    # Polygon Amoy testnet(chainId: 80001)\n                    '80001': {\n                        'exchange': '0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40',\n                        'negRiskAdapter': '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296',\n                        'negRiskExchange': '0xC5d563A36AE78145C45a50134d48A1215220f80a',\n                        'collateral': '0x9c4e1703476e875070ee25b56a58b008cfb8fa78',\n                        'conditionalTokens': '0x69308FB512518e39F9b16112fA8d994F4e2Bf8bB',\n                    },\n                    # Polygon mainnet(chainId: 137)\n                    '137': {\n                        'exchange': '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E',\n                        'negRiskAdapter': '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296',\n                        'negRiskExchange': '0xC5d563A36AE78145C45a50134d48A1215220f80a',\n                        'collateral': '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',\n                        'conditionalTokens': '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045',\n                    },\n                },\n            },\n            'exceptions': {\n                'exact': {\n                    # HTTP status codes\n                    '400': BadRequest,  # Bad Request - Invalid request parameters\n                    '401': AuthenticationError,  # Unauthorized - Invalid or missing authentication\n                    '403': PermissionDenied,  # Forbidden - Insufficient permissions\n                    '404': ExchangeError,  # Not Found - Resource not found\n                    '429': RateLimitExceeded,  # Too Many Requests - Rate limit exceeded\n                    '500': ExchangeError,  # Internal Server Error\n                    '502': ExchangeError,  # Bad Gateway\n                    '503': OnMaintenance,  # Service Unavailable - Service temporarily unavailable\n                    '504': NetworkError,  # Gateway Timeout\n                    # Common error messages(will be matched against error/message fields in response)\n                    'Invalid signature': AuthenticationError,  # Invalid signature in request\n                    'Invalid API key': AuthenticationError,  # Invalid or missing API key\n                    'Invalid timestamp': AuthenticationError,  # Invalid timestamp in request\n                    'Signature expired': AuthenticationError,  # Request timestamp is too old\n                    'Unauthorized': AuthenticationError,  # Authentication failed\n                    'Forbidden': PermissionDenied,  # Access denied\n                    'Rate limit exceeded': RateLimitExceeded,  # Rate limit exceeded\n                    'Too many requests': RateLimitExceeded,  # Too many requests\n                    'Invalid order': InvalidOrder,  # Order validation failed\n                    'Invalid orderID': OrderNotFound,  # Order does not exist\n                    'Order not found': OrderNotFound,  # Order does not exist\n                    'Insufficient funds': InsufficientFunds,  # Insufficient balance\n                    'Insufficient balance': InsufficientFunds,  # Insufficient balance\n                    'Invalid market': BadRequest,  # Invalid market/symbol\n                    'Invalid symbol': BadRequest,  # Invalid symbol\n                    'Market not found': BadRequest,  # Market does not exist\n                    'Service unavailable': ExchangeNotAvailable,  # Service temporarily unavailable\n                    'Maintenance': OnMaintenance,  # Service under maintenance\n                },\n                'broad': {\n                    'authentication': AuthenticationError,  # Any authentication-related error\n                    'authorization': PermissionDenied,  # Any authorization-related error\n                    'rate limit': RateLimitExceeded,  # Any rate limit error\n                    'invalid order': InvalidOrder,  # Any order validation error\n                    'insufficient': InsufficientFunds,  # Any insufficient funds/balance error\n                    'not found': ExchangeError,  # Any not found error\n                    'timeout': NetworkError,  # Any timeout error\n                    'network': NetworkError,  # Any network-related error\n                    'maintenance': OnMaintenance,  # Any maintenance-related error\n                },\n            },\n        })\n\n    def get_signature_type(self, params={}):\n        \"\"\"\n Helper method to get signature type from params or options with fallback to constants\n        :param dict [params]: parameters that may contain signatureType or signature_type\n        :returns number|None: signature type value\n        \"\"\"\n        signatureTypes = self.safe_dict(self.options, 'signatureTypes', {})\n        eoaSignatureType = self.safe_integer(signatureTypes, 'EOA')\n        polyProxySignatureType = self.safe_integer(signatureTypes, 'POLY_PROXY')\n        polyGnosisSafeSignatureType = self.safe_integer(signatureTypes, 'POLY_GNOSIS_SAFE')\n        # Note: POLY_GNOSIS_SAFE is not supported for now\n        proxyWalletAddress = self.get_proxy_wallet_address()\n        mainWalletAddress = self.get_main_wallet_address()\n        if proxyWalletAddress != mainWalletAddress:\n            return polyProxySignatureType\n        return eoaSignatureType\n\n    def get_side(self, sideString: str, params={}):\n        \"\"\"\n Helper method to get side from params or options with fallback to constants\n Converts BUY/SELL string to integer: BUY = 0, SELL = 1(matches UtilsBuy/UtilsSell from py-order-utils)\n        :param str sideString: side('BUY' or 'SELL')\n        :param dict [params]: parameters that may contain side or side_int\n        :returns number: side(0 for BUY, 1 for SELL)\n        \"\"\"\n        # Check if side_int is provided directly in params\n        sideInt = self.safe_integer(params, 'sideInt') or self.safe_integer(params, 'side_int')\n        if sideInt is not None:\n            return sideInt\n        # Get sides enum from options\n        sides = self.safe_dict(self.options, 'sides', {})\n        buySide = self.safe_integer(sides, 'BUY', 0)\n        sellSide = self.safe_integer(sides, 'SELL', 1)\n        # Convert side string to integer\n        sideUpper = sideString.upper()\n        sideValue = sellSide  # Default to SELL\n        if sideUpper == 'BUY':\n            sideValue = buySide\n        return sideValue\n\n    def fetch_markets(self, params={}) -> List[Market]:\n        \"\"\"\n        retrieves data on all markets for polymarket\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide#3-fetch-all-active-markets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param boolean [params.active]: fetch active markets only(default: True)\n        :param boolean [params.closed]: fetch closed markets\n        :returns dict[]: an array of objects representing market data\n        \"\"\"\n        limit = 500\n        options = self.safe_dict(self.options, 'fetchMarkets', {})\n        request: dict = self.extend({\n            'order': 'id',\n            'ascending': False,\n            'limit': limit,\n            'offset': 0,\n        }, params)\n        active = self.safe_bool(options, 'active', True)\n        if self.safe_value(params, 'closed') is None:\n            request['closed'] = not active\n        offset = self.safe_integer(request, 'offset', 0)\n        markets: List[Any] = []\n        while(True):\n            pageRequest = self.extend(request, {'offset': offset})\n            response = self.gamma_public_get_markets(pageRequest)\n            page = self.safe_list(response, 'data', response) or []\n            markets = self.array_concat(markets, page)\n            if len(page) < limit:\n                break\n            offset += limit\n        filtered = []\n        for i in range(0, len(markets)):\n            market = markets[i]\n            id = self.safe_string(market, 'id')\n            conditionId = self.safe_string(market, 'conditionId') or self.safe_string(market, 'condition_id')\n            if id is None and conditionId is None:\n                continue\n            filtered.append(market)\n        return self.parse_markets(filtered)\n\n    def parse_market(self, market: dict) -> Market:\n        # Schema uses 'conditionId'(camelCase)\n        conditionId = self.safe_string(market, 'conditionId')\n        question = self.safe_string(market, 'question')\n        # Schema uses 'questionID'(camelCase)\n        questionId = self.safe_string(market, 'questionID')\n        # Schema uses 'slug'(camelCase)\n        slug = self.safe_string(market, 'slug')\n        active = self.safe_bool(market, 'active', False)\n        closed = self.safe_bool(market, 'closed', False)\n        archived = self.safe_bool(market, 'archived', False)\n        outcomes = []\n        outcomePrices = []\n        outcomesStr = self.safe_string(market, 'outcomes')\n        if outcomesStr is not None:\n            parsedOutcomes = None\n            try:\n                parsedOutcomes = json.loads(outcomesStr)\n            except Exception as e:\n                parsedOutcomes = None\n            if parsedOutcomes is not None and len(parsedOutcomes) is not None:\n                for i in range(0, len(parsedOutcomes)):\n                    outcomes.append(parsedOutcomes[i])\n            else:\n                outcomesArray = outcomesStr.split(',')\n                for i in range(0, len(outcomesArray)):\n                    v = outcomesArray[i].strip()\n                    if v != '':\n                        outcomes.append(v)\n        outcomePricesStr = self.safe_string(market, 'outcomePrices')\n        if outcomePricesStr is not None:\n            parsedPrices = None\n            try:\n                parsedPrices = json.loads(outcomePricesStr)\n            except Exception as e:\n                parsedPrices = None\n            if parsedPrices is not None and len(parsedPrices) is not None:\n                for i in range(0, len(parsedPrices)):\n                    outcomePrices.append(self.parse_number(parsedPrices[i]))\n            else:\n                pricesArray = outcomePricesStr.split(',')\n                for i in range(0, len(pricesArray)):\n                    v = pricesArray[i].strip()\n                    if v != '':\n                        outcomePrices.append(self.parse_number(v))\n        # Use slug symbol if available\n        baseId = slug or conditionId\n        quoteId = self.safe_string(self.options, 'defaultCollateral', 'USDC')  # Polymarket uses USDC currency\n        # Market type - Polymarket is a prediction market platform\n        marketType: MarketType = 'option'  # Using 'option' match for prediction markets\n        ammType = self.safe_string(market, 'ammType')\n        # Schema uses 'enableOrderBook'(camelCase)\n        enableOrderBook = self.safe_bool(market, 'enableOrderBook', False)\n        # Market metadata\n        category = self.safe_string(market, 'category')\n        description = self.safe_string(market, 'description')\n        tags = self.safe_value(market, 'tags', [])\n        # Schema uses 'clobTokenIds'(camelCase) - can be string or array\n        clobTokenIds = self.safe_value(market, 'clobTokenIds')\n        if clobTokenIds is None:\n            clobTokenIds = []\n        if isinstance(clobTokenIds, str):\n            parsed = None\n            try:\n                parsed = json.loads(clobTokenIds)\n            except Exception as e:\n                parsed = None\n            if parsed is not None and parsed != None and len(parsed) is not None:\n                clobTokenIds = []\n                for i in range(0, len(parsed)):\n                    clobTokenIds.append(parsed[i])\n            else:\n                cleaned = clobTokenIds\n                cleaned = cleaned.replace('[', '').replace(']', '').replace('\"', '')\n                clobTokenIdsArray = cleaned.split(',')\n                clobTokenIds = []\n                for i in range(0, len(clobTokenIdsArray)):\n                    v = clobTokenIdsArray[i].strip()\n                    if v != '':\n                        clobTokenIds.append(v)\n        outcomesInfo = []\n        length = len(outcomes)\n        if len(outcomePrices) > length:\n            length = len(outcomePrices)\n        if len(clobTokenIds) > length:\n            length = len(clobTokenIds)\n        for i in range(0, length):\n            outcome = None\n            if i < len(outcomes):\n                outcome = outcomes[i]\n            price = None\n            if i < len(outcomePrices):\n                price = self.parse_number(outcomePrices[i])\n            clobId = None\n            if i < len(clobTokenIds):\n                clobId = clobTokenIds[i]\n            outcomeId = str(i)\n            if clobId is not None:\n                outcomeId = clobId\n            outcomesInfo.append({\n                'id': outcomeId,\n                'name': outcome,\n                'price': price,\n                'clobId': clobId,\n                'assetId': clobId,\n            })\n        # Parse dates - Schema uses 'endDateIso'(preferred) or 'endDate'(fallback)\n        endDateIso = self.safe_string(market, 'endDateIso') or self.safe_string(market, 'endDate')\n        # Schema uses 'createdAt'(camelCase)\n        createdAt = self.safe_string(market, 'createdAt')\n        createdTimestamp = None\n        if createdAt is not None:\n            createdTimestamp = self.parse8601(createdAt)\n        # Volume and liquidity\n        volume = self.safe_string(market, 'volume')\n        volumeNum = self.safe_number(market, 'volumeNum')\n        liquidity = self.safe_string(market, 'liquidity')\n        liquidityNum = self.safe_number(market, 'liquidityNum')\n        feesEnabled = self.safe_bool(market, 'feesEnabled', False)\n        makerBaseFee = self.safe_number(market, 'makerBaseFee')\n        takerBaseFee = self.safe_number(market, 'takerBaseFee')\n        base = baseId\n        quote = quoteId\n        settle = quote  # Use quote\n        # Parse expiry for option symbol formatting\n        # Handle date-only strings(YYYY-MM-DD) by converting to ISO8601 datetime\n        expiry = None\n        expiryDatetime = endDateIso\n        if endDateIso is not None:\n            dateString = endDateIso\n            # Check if it's a date-only string(YYYY-MM-DD format)\n            if dateString.find(':') < 0:\n                # Append time to make it a valid ISO8601 datetime\n                dateString = dateString + 'T00:00:00Z'\n            expiry = self.parse8601(dateString)\n        # Format symbol with expiry date(similar to binance/okx option format)\n        # Format: base/quote:settle-YYMMDD\n        symbol = base + '/' + quote\n        if expiry is not None:\n            ymd = self.yymmdd(expiry)\n            symbol = symbol + ':' + settle + '-' + ymd\n        # Prediction markets don't have strike prices or option types in the schema\n        # These fields are kept\n        strike = None\n        optionType = None\n        contractSize = self.parse_number('1')\n        # Calculate fees based on feesEnabled flag\n        takerFee = self.parse_number('0')\n        makerFee = self.parse_number('0')\n        if feesEnabled:\n            # Fees are enabled - use makerBaseFee and takerBaseFee from schema\n            # These are typically in basis points(e.g., 200 = 2% = 0.02)\n            if takerBaseFee is not None:\n                takerFee = takerBaseFee / 10000  # Convert basis points to decimal\n            if makerBaseFee is not None:\n                makerFee = makerBaseFee / 10000  # Convert basis points to decimal\n        created = self.milliseconds()  # TODO change it\n        if createdTimestamp is not None:\n            created = createdTimestamp\n        volumeValue = self.parse_number('0')\n        if volumeNum is not None:\n            volumeValue = volumeNum\n        elif volume is not None:\n            volumeValue = self.parse_number(volume)\n        liquidityValue = self.parse_number('0')\n        if liquidityNum is not None:\n            liquidityValue = liquidityNum\n        elif liquidity is not None:\n            liquidityValue = self.parse_number(liquidity)\n        return {\n            'id': conditionId,\n            'symbol': symbol,\n            'base': base,\n            'quote': quote,\n            'settle': settle,\n            'baseId': baseId,\n            'quoteId': quoteId,\n            'settleId': settle,\n            'type': marketType,\n            'spot': False,\n            'margin': False,\n            'swap': False,\n            'future': False,\n            'option': True,  # Prediction markets are treated\n            'active': enableOrderBook and active and not closed and not archived,\n            'contract': True,\n            'linear': None,\n            'inverse': None,\n            'contractSize': contractSize,\n            'expiry': expiry,\n            'expiryDatetime': expiryDatetime,\n            'strike': strike,\n            'optionType': optionType,\n            'taker': takerFee,\n            'maker': makerFee,\n            'precision': {\n                'amount': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n                'price': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n            },\n            'limits': {\n                'leverage': {\n                    'min': None,\n                    'max': None,\n                },\n                'amount': {\n                    'min': None,\n                    'max': None,\n                },\n                'price': {\n                    'min': 0,  # Prediction markets are 0-1\n                    'max': 1,  # Prediction markets are 0-1\n                },\n                'cost': {\n                    'min': None,\n                    'max': None,\n                },\n            },\n            'created': created,\n            'info': self.deep_extend(market, {\n                'outcomes': outcomes,\n                'outcomePrices': outcomePrices,\n                'outcomesInfo': outcomesInfo,\n                'question': question,\n                'slug': slug,\n                'category': category,\n                'description': description,\n                'tags': tags,\n                'condition_id': conditionId,\n                'question_id': questionId,\n                'asset_id': questionId,\n                'ammType': ammType,\n                'enableOrderBook': enableOrderBook,\n                'volume': volumeValue,\n                'liquidity': liquidityValue,\n                'endDateIso': endDateIso,\n                'createdAt': createdAt,\n                'createdTimestamp': createdTimestamp,\n                'clobTokenIds': clobTokenIds,\n                'quoteDecimals': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n                'baseDecimals': 6,  # https://github.com/Polymarket/clob-client/blob/main/src/config.ts\n            }),\n        }\n\n    def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:\n        \"\"\"\n        fetches the order book for a market\n\n        https://docs.polymarket.com/api-reference/orderbook/get-order-book-summary\n\n        :param str symbol: unified symbol of the market to fetch the order book for\n        :param int [limit]: the maximum amount of order book entries to return\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID for the specific outcome(required if market has multiple outcomes)\n        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols\n        \"\"\"\n        self.load_markets()\n        market = self.market(symbol)\n        request: dict = {}\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a token_id parameter for market ' + symbol)\n        request['token_id'] = tokenId\n        response = self.clob_public_get_orderbook_token_id(self.extend(request, params))\n        return self.parse_order_book(response, symbol)\n\n    def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks:\n        \"\"\"\n        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getorderbooks\n\n        :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None\n        :param int [limit]: the maximum amount of order book entries to return\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: a dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbol\n        \"\"\"\n        self.load_markets()\n        if symbols is None:\n            symbols = self.symbols\n        # Build list of token IDs to fetch order books for\n        tokenIds: List[str] = []\n        tokenIdToSymbol: dict = {}\n        for i in range(0, len(symbols)):\n            symbol = symbols[i]\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n                tokenIds.append(tokenId)\n                tokenIdToSymbol[tokenId] = symbol\n        if len(tokenIds) == 0:\n            return {}\n        # Fetch order books for all token IDs at once using POST /books endpoint\n        # See https://docs.polymarket.com/api-reference/orderbook/get-multiple-order-books-summaries-by-request\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: array of order book objects, each with asset_id matching token_id\n        requestBody = []\n        for i in range(0, len(tokenIds)):\n            requestItem: dict = {'token_id': tokenIds[i]}\n            if limit is not None:\n                requestItem['limit'] = limit\n            requestBody.append(requestItem)\n        response = self.clob_public_post_books(self.extend({'requests': requestBody}, params))\n        # Parse response: array of order book objects, each with asset_id field\n        # Response is directly an array: [{asset_id: \"...\", bids: [...], asks: [...]}, ...]\n        result: dict = {}\n        if isinstance(response, list):\n            for i in range(0, len(response)):\n                orderbookData = response[i]\n                assetId = self.safe_string(orderbookData, 'asset_id')\n                symbol = tokenIdToSymbol[assetId]\n                if symbol is not None:\n                    try:\n                        orderbook = self.parse_order_book(orderbookData, symbol)\n                        result[symbol] = orderbook\n                    except Exception as e:\n                        # Skip markets that fail to parse\n                        continue\n        return result\n\n    def parse_order_book(self, orderbook: dict, symbol: Str = None, timestamp: Int = None, bidsKey: Str = 'bids', asksKey: Str = 'asks', priceKey: Int = 0, amountKey: Int = 1, countOrIdKey: Int = 2) -> OrderBook:\n        # Polymarket CLOB orderbook format(from /book endpoint)\n        # See https://docs.polymarket.com/developers/CLOB/clients/methods-public#getorderbook\n        # {\n        #   \"market\": \"string\",\n        #   \"asset_id\": \"string\",\n        #   \"timestamp\": \"string\",\n        #   \"bids\": [\n        #     {\n        #       \"price\": \"0.65\",  # string\n        #       \"size\": \"100\"     # string\n        #     }\n        #   ],\n        #   \"asks\": [\n        #     {\n        #       \"price\": \"0.66\",  # string\n        #       \"size\": \"50\"      # string\n        #     }\n        #   ],\n        #   \"min_order_size\": \"string\",\n        #   \"tick_size\": \"string\",\n        #   \"neg_risk\": boolean,\n        #   \"hash\": \"string\"\n        # }\n        # Note: Ensure bids and asks are always arrays to avoid Python transpilation issues\n        # safeList can return None, which becomes None in Python, causing len() to fail\n        bids = self.safe_list(orderbook, 'bids', []) or []\n        asks = self.safe_list(orderbook, 'asks', []) or []\n        # Note: Using 'const' without explicit type annotation to avoid Python transpilation issues\n        # The transpiler incorrectly preserves TypeScript tuple type annotations(e.g., ': [number, number][]') in Python code\n        parsedBids = []\n        parsedAsks = []\n        for i in range(0, len(bids)):\n            bid = bids[i]\n            price = self.safe_number(bid, 'priceNumber', self.safe_number(bid, 'price'))\n            amount = self.safe_number(bid, 'sizeNumber', self.safe_number(bid, 'size'))\n            if price is not None and amount is not None:\n                parsedBids.append([price, amount])\n        for i in range(0, len(asks)):\n            ask = asks[i]\n            price = self.safe_number(ask, 'priceNumber', self.safe_number(ask, 'price'))\n            amount = self.safe_number(ask, 'sizeNumber', self.safe_number(ask, 'size'))\n            if price is not None and amount is not None:\n                parsedAsks.append([price, amount])\n        # Extract timestamp from orderbook response if available\n        orderbookTimestamp = self.safe_string(orderbook, 'timestamp')\n        finalTimestamp = timestamp\n        if orderbookTimestamp is not None:\n            # CLOB API returns timestamp string, convert to milliseconds\n            finalTimestamp = self.parse8601(orderbookTimestamp)\n        # Extract tick_size and neg_risk from orderbook if available(useful metadata)\n        # These are also available via get_tick_size() and get_neg_risk() endpoints\n        # Based on py-clob-client: get_tick_size() and get_neg_risk()\n        tickSize = self.safe_string(orderbook, 'tick_size')\n        negRisk = self.safe_bool(orderbook, 'neg_risk')\n        minOrderSize = self.safe_string(orderbook, 'min_order_size')\n        result: OrderBook = {\n            'symbol': symbol,\n            'bids': parsedBids,\n            'asks': parsedAsks,\n            'timestamp': finalTimestamp,\n            'datetime': self.iso8601(finalTimestamp),\n            'nonce': None,\n        }\n        # Include tick_size, neg_risk, and min_order_size in info if available(useful metadata)\n        if tickSize is not None or negRisk is not None or minOrderSize is not None:\n            metadata: dict = {}\n            if tickSize is not None:\n                metadata['tick_size'] = tickSize\n            if negRisk is not None:\n                metadata['neg_risk'] = negRisk\n            if minOrderSize is not None:\n                metadata['min_order_size'] = minOrderSize\n            result['info'] = self.extend(orderbook, metadata)\n        return result\n\n    def fetch_ticker(self, symbol: str, params={}) -> Ticker:\n        \"\"\"\n        fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market\n\n        https://docs.polymarket.com/api-reference/pricing/get-market-price\n        https://docs.polymarket.com/api-reference/pricing/get-midpoint-price\n\n        :param str symbol: unified symbol of the market to fetch the ticker for\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID for the specific outcome(required if market has multiple outcomes)\n        :param str [params.side]: the side: 'BUY' or 'SELL'(default: 'BUY')\n        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`\n\n **Currently Populated Fields:**\n - `bid` - Best bid price from POST /prices endpoint(BUY side)\n - `ask` - Best ask price from POST /prices endpoint(SELL side)\n - `last` - Midpoint price from GET /midpoint or lastTradePrice from market info\n - `open` - Calculated approximation: last / (1 + oneDayPriceChange)\n - `change` - Calculated: last - open\n - `percentage` - From oneDayPriceChange * 100(from market info)\n - `volume` - From volumeNum or volume(from market info)\n - `timestamp` - From updatedAt(parsed from ISO string)\n - `datetime` - ISO8601 formatted timestamp\n\n **Currently Undefined Fields(Available via Additional API Calls):**\n - `high` - Can be fetched from GET /prices-history(24h price history) or GET /trades(24h trades)\n - `low` - Can be fetched from GET /prices-history(24h price history) or GET /trades(24h trades)\n - `bidVolume` - Can be calculated from GET /book(order book) by summing all bid sizes\n - `askVolume` - Can be calculated from GET /book(order book) by summing all ask sizes\n - `vwap` - Can be calculated from GET /trades(24h trades) using volume-weighted average\n - `average` - Not available\n - `indexPrice` - Not available\n - `markPrice` - Not available\n\n **Enhancement Options:**\n\n 1. **For High/Low/More Accurate Open:**\n    - Use fetchOHLCV() to get 24h price history: `await exchange.fetchOHLCV(symbol, '1h', since24hAgo, None, {token_id: tokenId})`\n    - Calculate high/low from OHLCV data\n    - Use first candle's open price for accurate 24h open\n    - API: GET /prices-history(see https://docs.polymarket.com/developers/CLOB/timeseries)\n\n 2. **For VWAP:**\n    - Use fetchTrades() to get 24h trades: `await exchange.fetchTrades(symbol, since24hAgo, None, {token_id: tokenId})`\n    - Calculate: vwap = sum(trade.cost) / sum(trade.amount)\n    - API: GET /trades(see https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets)\n\n 3. **For Bid/Ask Volumes:**\n    - Use fetchOrderBook() to get order book: `await exchange.fetchOrderBook(symbol, None, {token_id: tokenId})`\n    - Calculate: bidVolume = sum of all bid[1](sizes), askVolume = sum of all ask[1](sizes)\n    - API: GET /book(see https://docs.polymarket.com/api-reference/orderbook/get-order-book-summary)\n\n 4. **For More Accurate Last Price:**\n    - Use GET /last-trade-price endpoint: `await exchange.clobPublicGetLastTradePrice({token_id: tokenId})`\n    - API: GET /last-trade-price(see https://docs.polymarket.com/api-reference/trades/get-last-trade-price)\n        \"\"\"\n        self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchTicker() requires a token_id parameter for market ' + symbol)\n        # Fetch prices using POST /prices endpoint with both BUY and SELL sides\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        pricesResponse = self.clob_public_post_prices(self.extend({\n            'requests': [\n                {'token_id': tokenId, 'side': 'BUY'},\n                {'token_id': tokenId, 'side': 'SELL'},\n            ],\n        }, params))\n        # Parse prices response: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        tokenPrices = self.safe_dict(pricesResponse, tokenId, {})\n        buyPrice = self.safe_string(tokenPrices, 'BUY')\n        sellPrice = self.safe_string(tokenPrices, 'SELL')\n        # Fetch midpoint if available(optional, ignore if not provided)\n        midpoint = None\n        try:\n            midpointResponse = self.clob_public_get_midpoint(self.extend({'token_id': tokenId}, params))\n            midpoint = self.safe_string(midpointResponse, 'mid')\n        except Exception as e:\n            # Ignore midpoint if not available or fails\n            midpoint = None\n        # Combine pricing data with market info - already loaded from fetchMarkets\n        combinedData = self.deep_extend(marketInfo, {\n            'buyPrice': buyPrice,\n            'sellPrice': sellPrice,\n            'midpoint': midpoint,\n        })\n        return self.parse_ticker(combinedData, market)\n\n    def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:\n        \"\"\"\n        fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market\n\n        https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices\n\n        :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param boolean [params.fetchSpreads]: if True, also fetch bid-ask spreads for all markets(default: False)\n        :returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`\n        \"\"\"\n        self.load_markets()\n        # Build list of token IDs to fetch prices for\n        tokenIds: List[str] = []\n        tokenIdToSymbol: dict = {}\n        symbolsToFetch = symbols or self.symbols\n        for i in range(0, len(symbolsToFetch)):\n            symbol = symbolsToFetch[i]\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n                tokenIds.append(tokenId)\n                tokenIdToSymbol[tokenId] = symbol\n        if len(tokenIds) == 0:\n            return {}\n        # Build requests array for POST /prices endpoint\n        # Each token needs both BUY and SELL sides\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        requests = []\n        for i in range(0, len(tokenIds)):\n            tokenId = tokenIds[i]\n            requests.append({'token_id': tokenId, 'side': 'BUY'})\n            requests.append({'token_id': tokenId, 'side': 'SELL'})\n        # Fetch prices for all token IDs at once using POST /prices endpoint\n        # Response format: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        pricesResponse = self.clob_public_post_prices(self.extend({'requests': requests}, params))\n        # Optionally fetch spreads for all token IDs\n        # See https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spreads\n        fetchSpreads = self.safe_bool(params, 'fetchSpreads', False)\n        spreadsResponse = {}\n        if fetchSpreads:\n            try:\n                spreadsResponse = self.clob_public_post_spreads(self.extend({'token_ids': tokenIds}, params))\n            except Exception as e:\n                spreadsResponse = {}\n        # Build market data map for efficient lookup\n        tokenIdToMarket = {}\n        for i in range(0, len(tokenIds)):\n            tokenId = tokenIds[i]\n            symbol = tokenIdToSymbol[tokenId]\n            tokenIdToMarket[tokenId] = self.market(symbol)\n        # Parse prices and build tickers(no additional fetching during parsing)\n        tickers: dict = {}\n        for i in range(0, len(tokenIds)):\n            tokenId = tokenIds[i]\n            symbol = tokenIdToSymbol[tokenId]\n            market = tokenIdToMarket[tokenId]\n            try:\n                # Get prices from the response(both BUY and SELL are in the same response)\n                tokenPrices = self.safe_dict(pricesResponse, tokenId, {})\n                buyPrice = self.safe_string(tokenPrices, 'BUY')\n                sellPrice = self.safe_string(tokenPrices, 'SELL')\n                # Get spread if available\n                spread = self.safe_string(spreadsResponse, tokenId)\n                # Use market info data(already loaded from fetchMarkets)\n                marketInfo = self.safe_dict(market, 'info', {})\n                # Combine pricing data with market info\n                combinedData = self.deep_extend(marketInfo, {\n                    'buyPrice': buyPrice,\n                    'sellPrice': sellPrice,\n                    'spread': spread,\n                })\n                ticker = self.parse_ticker(combinedData, market)\n                tickers[symbol] = ticker\n            except Exception as e:\n                # Skip markets that fail to parse\n                continue\n        return tickers\n\n    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:\n        \"\"\"\n        parses a ticker data structure from Polymarket API response\n        :param dict ticker: ticker data structure from Polymarket API\n        :param dict [market]: market structure\n        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`\n\n **Data Sources:**\n - Market info from fetchMarkets()(volume, oneDayPriceChange, lastTradePrice, etc.)\n - Pricing API(buyPrice, sellPrice, midpoint)\n - Market metadata(updatedAt, volume24hr, volume1wk, volume1mo, volume1yr)\n\n **Currently Parsed Fields:**\n - `bid` - From buyPrice(POST /prices BUY side) or bestBid(market info)\n - `ask` - From sellPrice(POST /prices SELL side) or bestAsk(market info)\n - `last` - From midpoint(GET /midpoint) or lastTradePrice(market info)\n - `open` - Calculated: last / (1 + oneDayPriceChange) when both available\n - `change` - Calculated: last - open\n - `percentage` - From oneDayPriceChange * 100\n - `volume` - From volumeNum or volume(market info)\n - `timestamp` - From updatedAt(ISO string parsed to milliseconds)\n - `datetime` - ISO8601 formatted timestamp\n\n **Fields Set to Undefined(Can Be Enhanced):**\n - `high` - Not available in current data sources. Can be calculated from:\n   - Price history: Math.max(...ohlcvData.map(c => c[2])) where c[2] is high\n   - Trades: Math.max(...trades.map(t => t.price))\n - `low` - Not available in current data sources. Can be calculated from:\n   - Price history: Math.min(...ohlcvData.map(c => c[3])) where c[3] is low\n   - Trades: Math.min(...trades.map(t => t.price))\n - `bidVolume` - Not available. Can be calculated from order book:\n   - orderbook.bids.reduce((sum, bid) => sum + bid[1], 0)\n - `askVolume` - Not available. Can be calculated from order book:\n   - orderbook.asks.reduce((sum, ask) => sum + ask[1], 0)\n - `vwap` - Not available. Can be calculated from trades:\n   - totalCost = trades.reduce((sum, t) => sum + t.cost, 0)\n   - totalVolume = trades.reduce((sum, t) => sum + t.amount, 0)\n   - vwap = totalCost / totalVolume\n\n **To Enhance Ticker Data:**\n Before calling parseTicker(), you can fetch additional data and add it to the ticker dict:\n\n ```typescript\n  # Example: Add high/low from price history\n since24h = exchange.milliseconds() - 24 * 60 * 60 * 1000\n ohlcv = exchange.fetchOHLCV(symbol, '1h', since24h, None, {token_id: tokenId})\n if len(ohlcv) > 0:\n     highs = ohlcv.map(c => c[2])  # OHLCV[2] is high\n     lows = ohlcv.map(c => c[3])  # OHLCV[3] is low\n     ticker['high'] = Math.max(...highs)\n     ticker['low'] = Math.min(...lows)\n     ticker['open'] = ohlcv[0][1]  # First candle's open\n}\n\n  # Example: Add VWAP from trades\n trades = exchange.fetchTrades(symbol, since24h, None, {token_id: tokenId})\n if len(trades) > 0:\n     totalCost = 0\n     totalVolume = 0\n     for i in range(0, len(trades)):\n         totalCost += trades[i]['cost']\n         totalVolume += trades[i]['amount']\n     }\n     ticker['vwap'] = totalVolume > totalCost / totalVolume if 0 else None\n}\n\n  # Example: Add bid/ask volumes from order book\n orderbook = exchange.fetchOrderBook(symbol, None, {token_id: tokenId})\n bidVolume = 0\n askVolume = 0\n for i in range(0, len(orderbook['bids'])):\n     bidVolume += orderbook['bids'][i][1]\n}\n for i in range(0, len(orderbook['asks'])):\n     askVolume += orderbook['asks'][i][1]\n}\n ticker['bidVolume'] = bidVolume\n ticker['askVolume'] = askVolume\n ```\n        \"\"\"\n        # Polymarket ticker format from market data\n        symbol = market['symbol'] if market else None\n        # Parse outcome prices\n        outcomePricesStr = self.safe_string(ticker, 'outcomePrices')\n        outcomePrices = []\n        if outcomePricesStr:\n            try:\n                parsed = json.loads(outcomePricesStr)\n                # Note: Ensure all elements are numbers - json.loadsmay return strings\n                # Convert each element to a number to avoid Python multiplication errors\n                if parsed is not None and parsed != None and len(parsed) is not None:\n                    for i in range(0, len(parsed)):\n                        price = self.parse_number(parsed[i])\n                        if price is not None:\n                            outcomePrices.append(price)\n            except Exception as e:\n                # Note: Using for loop instead of .map() to avoid Python transpilation issues\n                # Arrow functions with type annotations(e.g., '(p: string) =>') are incorrectly preserved in Python\n                pricesArray = outcomePricesStr.split(',')\n                for i in range(0, len(pricesArray)):\n                    price = self.parse_number(pricesArray[i].strip())\n                    if price is not None:\n                        outcomePrices.append(price)\n        last = None\n        bid = None\n        ask = None\n        high = None\n        low = None\n        # Volume data\n        volume = self.safe_number(ticker, 'volumeNum', self.safe_number(ticker, 'volume'))\n        volume24hr = self.safe_number(ticker, 'volume24hr')\n        volume1wk = self.safe_number(ticker, 'volume1wk')\n        volume1mo = self.safe_number(ticker, 'volume1mo')\n        volume1yr = self.safe_number(ticker, 'volume1yr')\n        # Price changes\n        oneDayPriceChange = self.safe_number(ticker, 'oneDayPriceChange')\n        # Best bid/ask from pricing API(BUY = bid, SELL = ask)\n        buyPrice = self.safe_number(ticker, 'buyPrice')\n        sellPrice = self.safe_number(ticker, 'sellPrice')\n        midpoint = self.safe_number(ticker, 'midpoint')\n        # Use pricing API data if available\n        if buyPrice is not None:\n            bid = buyPrice\n        if sellPrice is not None:\n            ask = sellPrice\n        if midpoint is not None:\n            last = midpoint\n        # Fallback to ticker data if pricing API data not available\n        bestBid = self.safe_number(ticker, 'bestBid')\n        bestAsk = self.safe_number(ticker, 'bestAsk')\n        lastTradePrice = self.safe_number(ticker, 'lastTradePrice')\n        if bid is None and bestBid is not None:\n            bid = bestBid\n        if ask is None and bestAsk is not None:\n            ask = bestAsk\n        if last is None and lastTradePrice is not None:\n            last = lastTradePrice\n        # Timestamp\n        updatedAtString = self.safe_string(ticker, 'updatedAt')\n        timestamp = self.parse8601(updatedAtString) if updatedAtString else None\n        datetime = self.iso8601(timestamp) if timestamp else None\n        # Open(previous closing price - approximated)\n        open = last is not None and oneDayPriceChange is not last / (1 + oneDayPriceChange) if None else None\n        # Change and percentage\n        change = last is not None and open is not last - open if None else None\n        percentage = oneDayPriceChange is not oneDayPriceChange * 100 if None else None\n        # Add additional Polymarket-specific fields to info\n        tickerInfo = self.safe_dict(ticker, 'info', {})\n        extendedInfo = self.deep_extend(tickerInfo, {\n            'buyPrice': buyPrice,\n            'sellPrice': sellPrice,\n            'midpoint': midpoint,\n            'lastTradePrice': lastTradePrice,\n            'volume24hr': volume24hr,\n            'volume1wk': volume1wk,\n            'volume1mo': volume1mo,\n            'volume1yr': volume1yr,\n        })\n        return {\n            'symbol': symbol,\n            'info': self.deep_extend(ticker, {'info': extendedInfo}),\n            'timestamp': timestamp,\n            'datetime': datetime,\n            'high': high,\n            'low': low,\n            'bid': bid,\n            'bidVolume': None,\n            'ask': ask,\n            'askVolume': None,\n            'vwap': None,\n            'open': open,\n            'close': last,\n            'last': last,\n            'previousClose': open,\n            'change': change,\n            'percentage': percentage,\n            'average': None,\n            'baseVolume': volume,\n            'quoteVolume': volume,\n            'indexPrice': None,\n            'markPrice': None,\n        }\n\n    def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        get the list of most recent trades for a particular symbol\n\n        https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n\n        :param str symbol: unified symbol of the market to fetch trades for\n        :param int [since]: timestamp in ms of the earliest trade to fetch\n        :param int [limit]: the maximum amount of trades to fetch(default: 100, max: 10000)\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.offset]: offset for pagination(default: 0, max: 10000)\n        :param boolean [params.takerOnly]: if True, returns only trades where the user is the taker(default: True)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`\n        \"\"\"\n        self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get condition_id from market info(self is the market ID for Polymarket)\n        conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n        request: dict = {\n            'market': [conditionId],  # Data API expects an array of condition IDs\n        }\n        # Note: Data API /trades endpoint supports limit(default: 100, max: 10000) and offset for pagination\n        # The 'since' parameter is not directly supported by the REST API\n        if limit is not None:\n            request['limit'] = min(limit, 10000)  # Cap at max 10000\n        offset = self.safe_integer(params, 'offset')\n        if offset is not None:\n            request['offset'] = offset\n        takerOnly = self.safe_bool(params, 'takerOnly', True)\n        request['takerOnly'] = takerOnly\n        side = self.safe_string_upper(params, 'side')\n        if side is not None:\n            request['side'] = side\n        response = self.data_public_get_trades(self.extend(request, self.omit(params, ['offset', 'takerOnly', 'side'])))\n        tradesData = []\n        if isinstance(response, list):\n            tradesData = response\n        else:\n            dataList = self.safe_list(response, 'data', [])\n            if dataList is not None:\n                tradesData = dataList\n        return self.parse_trades(tradesData, market, since, limit)\n\n    def parse_trade(self, trade: dict, market: Market = None) -> Trade:\n        # Detect Data API format(has conditionId field) vs CLOB format(has market/asset_id fields)\n        # Check for both camelCase and snake_case for robustness\n        conditionId = self.safe_string_2(trade, 'conditionId', 'condition_id')\n        isDataApiFormat = conditionId is not None\n        if isDataApiFormat:\n            # Data API format: https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n            # {\n            #   \"proxyWallet\": \"0x...\",\n            #   \"side\": \"BUY\",\n            #   \"asset\": \"<string>\",\n            #   \"conditionId\": \"0x...\",\n            #   \"size\": 123,\n            #   \"price\": 123,\n            #   \"timestamp\": 123,\n            #   \"transactionHash\": \"0x...\",\n            #   ...\n            # }\n            # Use transactionHash, check both camelCase and snake_case\n            id = self.safe_string_2(trade, 'transactionHash', 'transaction_hash')\n            symbol = None\n            if market is not None and market['symbol'] is not None:\n                symbol = market['symbol']\n            elif conditionId is not None:\n                resolved = self.safe_market(conditionId, None)\n                resolvedSymbol = self.safe_string(resolved, 'symbol')\n                if resolvedSymbol is not None:\n                    symbol = resolvedSymbol\n                else:\n                    symbol = conditionId\n            timestampSeconds = self.safe_integer(trade, 'timestamp')\n            timestamp = None\n            if timestampSeconds is not None:\n                timestamp = timestampSeconds * 1000\n            side = self.safe_string_lower(trade, 'side')\n            price = self.safe_number(trade, 'price')\n            amount = self.safe_number(trade, 'size')\n            cost = None\n            if price is not None and amount is not None:\n                cost = price * amount\n            # Data API doesn't provide fee information\n            return {\n                'id': id,\n                'info': trade,\n                'timestamp': timestamp,\n                'datetime': self.iso8601(timestamp),\n                'symbol': symbol,\n                'type': None,\n                'side': side,\n                'takerOrMaker': None,  # Data API doesn't provide self information\n                'price': price,\n                'amount': amount,\n                'cost': cost,\n                'fee': None,  # Data API doesn't provide fee information\n                'order': None,  # Data API doesn't provide order ID\n            }\n        else:\n            # CLOB Trade format(backward compatibility)\n            # interface Trade {\n            #   id: string\n            #   taker_order_id: string\n            #   market: string\n            #   asset_id: string\n            #   side: Side\n            #   size: string\n            #   fee_rate_bps: string\n            #   price: string\n            #   status: string\n            #   match_time: string\n            #   last_update: string\n            #   outcome: string\n            #   bucket_index: number\n            #   owner: string\n            #   maker_address: string\n            #   maker_orders: MakerOrder[]\n            #   transaction_hash: string\n            #   trader_side: \"TAKER\" | \"MAKER\"\n            # }\n            id = self.safe_string(trade, 'id')\n            assetId = self.safe_string(trade, 'asset_id')\n            tradeMarket = self.safe_string(trade, 'market')\n            symbol = None\n            if market is not None and market['symbol'] is not None:\n                symbol = market['symbol']\n            elif tradeMarket is not None:\n                resolved = self.safe_market(tradeMarket, None)\n                resolvedSymbol = self.safe_string(resolved, 'symbol')\n                if resolvedSymbol is not None:\n                    symbol = resolvedSymbol\n                else:\n                    symbol = tradeMarket\n            elif assetId is not None:\n                resolved = self.safe_market(assetId, market)\n                resolvedSymbol = self.safe_string(resolved, 'symbol')\n                if resolvedSymbol is not None:\n                    symbol = resolvedSymbol\n                else:\n                    symbol = assetId\n            matchTime = self.safe_integer(trade, 'match_time')\n            timestamp = None\n            if matchTime is not None:\n                timestamp = matchTime * 1000\n            # Top-level fields are from the taker perspective; for maker trades use maker_orders\n            side = self.safe_string_lower(trade, 'side')\n            price = self.safe_number(trade, 'price')\n            amount = self.safe_number(trade, 'size')\n            feeRateBps = self.safe_number(trade, 'fee_rate_bps')\n            traderSide = self.safe_string_upper(trade, 'trader_side')\n            if traderSide == 'MAKER':\n                makerOrders = self.safe_value(trade, 'maker_orders', [])\n                proxyWallet = self.get_proxy_wallet_address()\n                userAddress = proxyWallet.lower()\n                matched = False\n                for i in range(0, len(makerOrders)):\n                    m = makerOrders[i]\n                    mAddr = self.safe_string(m, 'maker_address')\n                    if mAddr is not None:\n                        mAddrLower = mAddr.lower()\n                        if mAddrLower == userAddress:\n                                price = self.safe_number(m, 'price')\n                                amount = self.safe_number(m, 'matched_amount')\n                                side = self.safe_string_lower(m, 'side')\n                                feeRateBps = self.safe_number(m, 'fee_rate_bps')\n                                matched = True\n                                break\n                if not matched:\n                    m = makerOrders[0]\n                    price = self.safe_number(m, 'price')\n                    amount = self.safe_number(m, 'matched_amount')\n                    side = self.safe_string_lower(m, 'side')\n                    feeRateBps = self.safe_number(m, 'fee_rate_bps')\n            feeCost = None\n            if feeRateBps is not None and price is not None and amount is not None:\n                feeCost = price * amount * feeRateBps / 10000\n            fee = None\n            if feeCost is not None:\n                fee = {\n                    'cost': feeCost,\n                    'currency': self.safe_string(self.options, 'defaultCollateral', 'USDC'),\n                    'rate': feeRateBps is not feeRateBps / 10000 if None else None,\n                }\n            cost = price * amount if (price is not None and amount is not None) else None\n            return {\n                'id': id,\n                'info': trade,\n                'timestamp': timestamp,\n                'datetime': self.iso8601(timestamp),\n                'symbol': symbol,\n                'type': None,\n                'side': side,\n                'takerOrMaker': self.safe_string_lower(trade, 'trader_side'),\n                'price': price,\n                'amount': amount,\n                'cost': cost,\n                'fee': fee,\n                'order': self.safe_string(trade, 'taker_order_id'),\n            }\n\n    def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}) -> List[list]:\n        \"\"\"\n        fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getpriceshistory\n\n        :param str symbol: unified symbol of the market to fetch OHLCV data for\n        :param str timeframe: the length of time each candle represents\n        :param int [since]: timestamp in ms of the earliest candle to fetch\n        :param int [limit]: the maximum amount of candles to fetch\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID for the specific outcome(required if market has multiple outcomes)\n        :param int [params.endTs]: timestamp in seconds for the ending date filter\n        :param number [params.fidelity]: data fidelity/quality\n        :returns int[][]: A list of candles ordered, open, high, low, close, volume\n        \"\"\"\n        self.load_markets()\n        market = self.market(symbol)\n        request: dict = {}\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            marketInfo = self.safe_dict(market, 'info', {})\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a token_id parameter for market ' + symbol)\n        request['market'] = tokenId  # API uses 'market' parameter for token_id\n        # Note: REST API /prices-history endpoint requires either:\n        # 1. startTs and endTs(mutually exclusive with interval)\n        # 2. interval(mutually exclusive with startTs/endTs)\n        # See https://docs.polymarket.com/developers/CLOB/timeseries\n        # Supported intervals: \"1m\", \"1h\", \"6h\", \"1d\", \"1w\", \"max\"\n        # CCXT will automatically reject unsupported timeframes based on the 'timeframes' definition\n        endTs = self.safe_integer(params, 'endTs')\n        if since is not None or endTs is not None:\n            # Use startTs/endTs when time range is specified\n            if since is not None:\n                # Convert milliseconds to seconds for API\n                request['startTs'] = self.parse_to_int(since / 1000)\n            if endTs is not None:\n                request['endTs'] = endTs\n        else:\n            # Use interval when no time range is specified\n            # CCXT will validate the timeframe against the 'timeframes' definition\n            # Map to API format(timeframe should already be validated by CCXT)\n            request['interval'] = timeframe\n        # Fidelity parameter controls data granularity(e.g., 720 for 12-hour intervals)\n        # If not provided, API may use default fidelity\n        fidelity = self.safe_number(params, 'fidelity')\n        # Polymarket enforces minimum fidelity per interval(e.g. interval=1m requires fidelity>=10)\n        # Avoid leaking a server-side validation error back to the user when a too-low value is supplied.\n        if timeframe == '1m':\n            if fidelity is None:\n                fidelity = 10\n            else:\n                fidelity = min(10, fidelity)\n        if fidelity is not None:\n            request['fidelity'] = fidelity\n        remainingParams = self.omit(params, ['token_id', 'endTs', 'fidelity'])\n        response = self.clob_public_get_prices_history(self.extend(request, remainingParams))\n        ohlcvData = []\n        if isinstance(response, list):\n            ohlcvData = response\n        else:\n            # Response has 'history' key containing the array\n            ohlcvData = self.safe_list(response, 'history', []) or []\n        return self.parse_ohlcvs(ohlcvData, market, timeframe, since, limit)\n\n    def parse_ohlcv(self, ohlcv: Any, market: Market = None) -> list:\n        # Polymarket MarketPrice format from getPricesHistory\n        # See https://docs.polymarket.com/developers/CLOB/clients/methods-public#getpriceshistory\n        # {\n        #   \"t\": number,  # timestamp in seconds\n        #   \"p\": number   # price\n        # }\n        # Note: Polymarket only returns price data, not full OHLCV\n        # We'll use the price, high, low, and close, with volume\n        timestamp = self.safe_integer(ohlcv, 't')\n        price = self.safe_number(ohlcv, 'p')\n        # Convert timestamp from seconds to milliseconds\n        if timestamp is not None:\n            timestamp = timestamp * 1000\n        return [\n            timestamp,\n            price,  # open\n            price,  # high(same since we only have price)\n            price,  # low(same since we only have price)\n            price,  # close\n            0,     # volume(not available in price history)\n        ]\n\n    def get_rounding_config(self, tickSize: str) -> dict:\n        \"\"\"\n        Get rounding configuration based on tick size(matches ROUNDING_CONFIG from official SDK)\n        :param str tickSize: tick size string(e.g., '0.1', '0.01', '0.001', '0.0001')\n        :returns dict: rounding configuration with price, size, and amount decimal places\n        \"\"\"\n        # Determine rounding config based on tick size(matches ROUNDING_CONFIG from SDK)\n        # Returns: {price: number, size: number, amount: number}\n        priceDecimals = 2\n        sizeDecimals = 2\n        amountDecimals = 4\n        if tickSize == '0.1':\n            priceDecimals = 1\n            sizeDecimals = 2\n            amountDecimals = 3\n        elif tickSize == '0.01':\n            priceDecimals = 2\n            sizeDecimals = 2\n            amountDecimals = 4\n        elif tickSize == '0.001':\n            priceDecimals = 3\n            sizeDecimals = 2\n            amountDecimals = 5\n        elif tickSize == '0.0001':\n            priceDecimals = 4\n            sizeDecimals = 2\n            amountDecimals = 6\n        return {\n            'price': priceDecimals,\n            'size': sizeDecimals,\n            'amount': amountDecimals,\n        }\n\n    def round_down(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Round down(truncate) a value to specific decimal places\n        :param str value: value to round down\n        :param number decimals: number of decimal places\n        :returns str: rounded down value\n        \"\"\"\n        return self.decimal_to_precision(value, 0, decimals, 2, 5)\n\n    def round_normal(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Round a value normally to specific decimal places\n        :param str value: value to round\n        :param number decimals: number of decimal places\n        :returns str: rounded value\n        \"\"\"\n        return self.decimal_to_precision(value, 1, decimals, 2, 5)\n\n    def round_up(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Round up a value to specific decimal places\n        :param str value: value to round up\n        :param number decimals: number of decimal places\n        :returns str: rounded up value\n        \"\"\"\n        return self.decimal_to_precision(value, 2, decimals, 2, 5)\n\n    def decimal_places(self, value: str) -> float:\n        \"\"\"\n        Count the number of decimal places in a string value\n        :param str value: value to count decimal places for\n        :returns number: number of decimal places\n        \"\"\"\n        parts = value.split('.')\n        if len(parts) == 2:\n            return len(parts[1])\n        return 0\n\n    def to_token_decimals(self, value: str, decimals: float) -> str:\n        \"\"\"\n        Convert a value to token decimals(smallest unit) by multiplying by 10^decimals and truncating\n        :param str value: value to convert\n        :param number decimals: number of decimals(e.g., 6 for USDC, 18 for tokens)\n        :returns str: value in smallest unit\n        \"\"\"\n        # Multiply by 10^decimals and truncate to integer\n        multiplier = self.integer_precision_to_amount(self.number_to_string(-decimals))\n        return Precise.string_div(Precise.string_mul(value, multiplier), '1', 0)\n\n    def build_and_sign_order(self, tokenId: str, side: str, size: str, price: str = None, market: Market = None, params={}) -> dict:\n        \"\"\"\n        Builds and signs an order with EIP-712 according to Polymarket order-utils specification\n\n        https://github.com/Polymarket/clob-order-utils\n        https://github.com/Polymarket/clob-client/blob/main/src/order-builder/builder.ts\n        https://github.com/Polymarket/python-order-utils/blob/main/py_order_utils/builders/order_builder.py\n\n        :param str tokenId: the token ID\n        :param str side: 'BUY' or 'SELL'\n        :param str size: order size\n        :param str [price]: order price(required for limit orders)\n        :param dict [market]: market structure(optional, used to get fees)\n        :param dict [params]: extra parameters\n        :param number [params.expiration]: expiration timestamp in seconds(default: 30 days from now)\n        :param number [params.nonce]: order nonce(default: 0)\n        :param number [params.feeRateBps]: fee rate in basis points(default: from market or 200 bps)\n        :param str [params.maker]: maker address(default: getMainWalletAddress())\n        :param str [params.taker]: taker address(default: zero address)\n        :param str [params.signer]: signer address(default: maker address)\n        :param number [params.signatureType]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :param str [params.orderType]: order type: 'GTC', 'IOC', 'FOK', 'GTD'(default: 'GTC' for limit orders, 'FOK' for market orders)\n        :returns dict: signed order object ready for submission\n        \"\"\"\n        # Get zero address constant(matches py-clob-client ZERO_ADDRESS)\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/constants.py\n        zeroAddress = self.safe_string(self.options, 'zeroAddress', '0x0000000000000000000000000000000000000000')\n        # Get signature type\n        signatureType = self.get_signature_type(params)\n        # Get maker address(wallet address) - checksummed for signing\n        maker = self.safe_string(params, 'maker')\n        if maker is None:\n            signatureTypes = self.safe_dict(self.options, 'signatureTypes', {})\n            eoaSignatureType = self.safe_integer(signatureTypes, 'EOA')\n            if signatureType == eoaSignatureType:\n                maker = self.get_main_wallet_address()\n            else:\n                maker = self.get_proxy_wallet_address()\n        normalizedMaker = self.normalize_address(maker)\n        # Get taker address(default: zero address for open orders)\n        taker = self.safe_string(params, 'taker', zeroAddress)\n        normalizedTaker = self.normalize_address(taker)\n        # Get fee rate in basis points from market or params\n        feeRateBps = self.safe_integer(params, 'feeRateBps')\n        if feeRateBps is None:\n            if market is not None:\n                # Try to get fee from market structure\n                marketInfo = self.safe_dict(market, 'info', {})\n                # First try takerBaseFee from market info(in basis points)\n                feeRateBps = self.safe_integer(marketInfo, 'takerBaseFee')\n                if feeRateBps is None:\n                    # Try taker fee from parsed market(decimal, convert to basis points)\n                    takerFee = self.safe_number(market, 'taker')\n                    if takerFee is not None:\n                        feeRateBps = int(round(takerFee * 10000))\n            # Fallback to default fee rate from options if not found in market\n            if feeRateBps is None:\n                feeRateBps = self.safe_integer(self.options, 'defaultFeeRateBps', 200)\n        # Get expiration(default: from options.defaultExpirationDays, or 30 days from now in seconds)\n        expiration = self.safe_integer(params, 'expiration')\n        if expiration is None:\n            nowSeconds = int(math.floor(self.milliseconds()) / 1000)\n            defaultExpirationDays = self.safe_integer(self.options, 'defaultExpirationDays', 30)\n            expiration = nowSeconds + (defaultExpirationDays * 24 * 60 * 60)\n        # Get nonce(default: current timestamp in seconds)\n        nonce = self.safe_integer(params, 'nonce')\n        if nonce is None:\n            nonce = 0  # Default nonce is 0\n        # Get signer address(default: maker address)\n        signer = self.safe_string(params, 'signer')\n        if signer is None:\n            signer = self.get_main_wallet_address()\n        normalizedSigner = self.normalize_address(signer)\n        # Generate salt(unique integer based on microseconds)\n        # Using microseconds for better uniqueness without relying on Math.random()\n        salt = int(math.floor(self.milliseconds()) / 1000)\n        # Calculate makerAmount and takerAmount from size and price\n        # Key steps: 1) Round down size first, 2) Calculate other amount, 3) Round if needed, 4) Convert to smallest units\n        # Get precision from market info or use defaults(USDC: 6 decimals, Tokens: 18 decimals)\n        orderMarketInfo: Any = {}\n        if market is not None:\n            orderMarketInfo = self.safe_dict(market, 'info', {})\n        marketPrecision: Any = {}\n        if market is not None:\n            marketPrecision = self.safe_dict(market, 'precision', {})\n        quoteDecimals = self.safe_integer(orderMarketInfo, 'quoteDecimals', self.safe_integer(marketPrecision, 'price'))\n        baseDecimals = self.safe_integer(orderMarketInfo, 'baseDecimals', self.safe_integer(marketPrecision, 'amount'))\n        defaultTickSize = self.safe_string(self.options, 'defaultTickSize')\n        tickSize = self.safe_string(orderMarketInfo, 'tick_size', defaultTickSize)\n        roundingConfig = self.get_rounding_config(tickSize)\n        priceDecimals = self.safe_integer(roundingConfig, 'price', 2)\n        sizeDecimals = self.safe_integer(roundingConfig, 'size', 2)\n        amountDecimals = self.safe_integer(roundingConfig, 'amount', 4)\n        makerAmount: str\n        takerAmount: str\n        isBuy = (side.upper() == 'BUY')\n        # Get price: from parameter, or from params.marketPrice for market orders\n        orderPrice = price\n        if orderPrice is None:\n            orderPrice = self.safe_string(params, 'marketPrice')\n        if orderPrice is None:\n            raise ArgumentsRequired(self.id + ' buildAndSignOrder() requires a price parameter or params.marketPrice')\n        # Round price and size first, then calculate amounts(same logic for limit and market orders)\n        rawPrice = self.round_normal(orderPrice, priceDecimals)\n        # Check if self is a market order for special decimal handling\n        orderType = self.safe_string(params, 'orderType', 'limit')\n        isMarketOrder = (orderType == 'market')\n        # Get rounding buffer constant\n        roundingBuffer = self.safe_integer(self.options, 'roundingBufferDecimals', 4)\n        # Determine decimal precision based on order type and side\n        makerDecimals = 0\n        takerDecimals = 0\n        if isMarketOrder:\n            # Get market order decimal limits for quote(USDC) and base(tokens)\n            marketOrderQuoteDecimals = self.safe_integer(self.options, 'marketOrderQuoteDecimals', 2)\n            marketOrderBaseDecimals = self.safe_integer(self.options, 'marketOrderBaseDecimals', 4)\n            if isBuy:\n                # Market buy orders: maker gives USDC(quote), taker gives tokens(base)\n                makerDecimals = marketOrderQuoteDecimals\n                takerDecimals = marketOrderBaseDecimals\n            else:\n                # Market sell orders: maker gives tokens(base), taker gives USDC(quote)\n                makerDecimals = marketOrderBaseDecimals\n                takerDecimals = marketOrderQuoteDecimals\n        else:\n            # Limit orders: use amountDecimals for both\n            makerDecimals = amountDecimals\n            takerDecimals = amountDecimals\n        if isBuy:\n            # BUY: maker gives USDC, wants tokens\n            # Round down size first\n            rawTakerAmt = self.round_down(size, sizeDecimals)\n            # Round taker amount to max decimals\n            if self.decimal_places(rawTakerAmt) > takerDecimals:\n                rawTakerAmt = self.round_down(rawTakerAmt, takerDecimals)\n            # Calculate maker amount: raw_maker_amt = raw_taker_amt * raw_price\n            # Do NOT round calculated amounts - preserve full precision for accurate calculations\n            # The decimal limits apply to input size and final representation, not intermediate calculations\n            rawMakerAmt = Precise.string_mul(rawTakerAmt, rawPrice)\n            # Convert to smallest units: maker gives USDC(quoteDecimals), taker gives tokens(baseDecimals)\n            makerAmount = self.to_token_decimals(rawMakerAmt, quoteDecimals)\n            takerAmount = self.to_token_decimals(rawTakerAmt, baseDecimals)\n        else:\n            # SELL: maker gives tokens, wants USDC\n            # Round down size first\n            rawMakerAmt = self.round_down(size, sizeDecimals)\n            # Round maker amount to max decimals\n            if self.decimal_places(rawMakerAmt) > makerDecimals:\n                rawMakerAmt = self.round_down(rawMakerAmt, makerDecimals)\n            # Calculate taker amount: raw_taker_amt = raw_maker_amt * raw_price\n            # Do NOT round calculated amounts - preserve full precision for accurate calculations\n            # The decimal limits apply to input size and final representation, not intermediate calculations\n            rawTakerAmt = Precise.string_mul(rawMakerAmt, rawPrice)\n            # Convert to smallest units: maker gives tokens(baseDecimals), taker gives USDC(quoteDecimals)\n            makerAmount = self.to_token_decimals(rawMakerAmt, baseDecimals)\n            takerAmount = self.to_token_decimals(rawTakerAmt, quoteDecimals)\n        sideInt = self.get_side(side, params)\n        order: dict = {\n            'salt': str(salt),  # uint256\n            'maker': normalizedMaker,  # address\n            'signer': normalizedSigner,  # address\n            'taker': normalizedTaker,  # address\n            'tokenId': str(tokenId),  # uint256\n            'makerAmount': str(makerAmount),  # uint256\n            'takerAmount': str(takerAmount),  # uint256\n            'expiration': str(expiration),  # uint256\n            'nonce': str(nonce),  # uint256\n            'feeRateBps': str(feeRateBps),  # uint256\n            'side': sideInt,  # uint8: number(0 or 1)\n            'signatureType': signatureType,  # uint8: number(0, 1, or 2)\n        }\n        chainId = self.safe_integer(self.options, 'chainId')\n        orderDomainName = self.safe_string(self.options, 'orderDomainName')\n        orderDomainVersion = self.safe_string(self.options, 'orderDomainVersion')\n        contractConfig = self.get_contract_config(chainId)\n        verifyingContract = self.normalize_address(self.safe_string(contractConfig, 'exchange'))\n        # Domain must match exactly what server expects for signature validation\n        domain = {\n            'name': orderDomainName,\n            'version': orderDomainVersion,\n            'chainId': chainId,\n            'verifyingContract': verifyingContract,\n        }\n        # EIP-712 types for orders from https://github.com/Polymarket/clob-order-utils/blob/main/src/exchange.order.const.ts\n        ORDER_STRUCTURE = [\n            {'name': 'salt', 'type': 'uint256'},\n            {'name': 'maker', 'type': 'address'},\n            {'name': 'signer', 'type': 'address'},\n            {'name': 'taker', 'type': 'address'},\n            {'name': 'tokenId', 'type': 'uint256'},\n            {'name': 'makerAmount', 'type': 'uint256'},\n            {'name': 'takerAmount', 'type': 'uint256'},\n            {'name': 'expiration', 'type': 'uint256'},\n            {'name': 'nonce', 'type': 'uint256'},\n            {'name': 'feeRateBps', 'type': 'uint256'},\n            {'name': 'side', 'type': 'uint8'},\n            {'name': 'signatureType', 'type': 'uint8'},\n        ]\n        # primary type is types[0] => 'primaryType': 'Order'\n        # EIP712Domain shouldn't be included in messageTypes\n        messageTypes: dict = {\n            'Order': ORDER_STRUCTURE,\n        }\n        signature = self.sign_typed_data(domain, messageTypes, order)\n        order['signature'] = signature\n        return order\n\n    def build_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict:\n        \"\"\"\n        build a signed order request payload from order parameters\n\n        https://docs.polymarket.com/developers/CLOB/orders/create-order\n        https://docs.polymarket.com/developers/CLOB/orders/create-order-batch\n\n        :param str symbol: unified symbol of the market to create an order in\n        :param str type: 'market' or 'limit'\n        :param str side: 'buy' or 'sell'\n        :param float amount: how much you want to trade in units of the base currency\n        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required if market has multiple outcomes)\n        :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK', 'GTD'(default: 'GTC')\n        :param str [params.clientOrderId]: a unique id for the order\n        :param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately\n        :param number [params.expiration]: expiration timestamp in seconds(default: 30 days from now)\n        :returns dict: request payload with order, owner, orderType, and optional fields\n        \"\"\"\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # Use first token ID if multiple outcomes exist\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' buildOrder() requires a token_id parameter for market ' + symbol)\n        # Convert CCXT side to Polymarket side(BUY/SELL)\n        polymarketSide = 'BUY' if (side == 'buy') else 'SELL'\n        # Convert amount and price to strings\n        size = self.number_to_string(amount)\n        priceStr = None\n        if type == 'limit':\n            if price is None:\n                raise ArgumentsRequired(self.id + ' buildOrder() requires a price parameter for limit orders')\n            priceStr = self.number_to_string(price)\n        elif type == 'market':\n            # For market orders, price is optional but recommended\n            # If not provided, we'll try to fetch from orderbook or use params.marketPrice\n            if price is not None:\n                priceStr = self.number_to_string(price)\n            else:\n                # Try to get price from params.marketPrice\n                marketPrice = self.safe_number(params, 'marketPrice')\n                if marketPrice is not None:\n                    priceStr = self.number_to_string(marketPrice)\n        # Determine orderType(at top level, not inside order object)\n        # Must be determined before building orderObject to set expiration correctly\n        orderType = self.safe_string(params, 'timeInForce', 'GTC')\n        if type == 'market':\n            # For market orders, use IOC(Immediate-Or-Cancel) by default\n            # IOC allows partial fills, making it more forgiving than FOK(Fill-Or-Kill)\n            # Users can still override with params.timeInForce = 'FOK' if needed\n            orderType = self.safe_string(params, 'timeInForce', 'IOC')\n        # Set expiration BEFORE signing: for non-GTD orders(GTC, FOK, FAK), expiration must be '0'\n        # Only GTD orders should have a timestamp expiration\n        # The signature must match the exact expiration value that will be sent to the API\n        # See https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n        orderTypeUpper = orderType.upper()\n        # For non-GTD orders, expiration MUST be '0'(API requirement)\n        # Override any user-provided expiration for non-GTD orders\n        orderParams = self.extend({}, params)\n        if orderTypeUpper == 'GTD':\n            expiration = self.safe_integer(params, 'expiration')\n            if expiration is None:\n                nowSeconds = int(math.floor(self.milliseconds()) / 1000)\n                defaultExpirationDays = self.safe_integer(self.options, 'defaultExpirationDays', 30)\n                expiration = nowSeconds + (defaultExpirationDays * 24 * 60 * 60)\n            else:\n                orderParams['expiration'] = str(expiration)\n        else:\n            # For non-GTD orders, expiration must be 0(will be converted to \"0\" string in signing)\n            orderParams['expiration'] = 0\n        # Pass order type to buildAndSignOrder for market order special handling\n        orderParams['orderType'] = type\n        # Build and sign the order with EIP-712(pass market to use fees from market)\n        signedOrder = self.build_and_sign_order(tokenId, polymarketSide, size, priceStr, market, orderParams)\n        # override signedOrder types\n        signedOrder['salt'] = self.parse_to_int(signedOrder['salt'])  # integer not string\n        signedOrder['side'] = polymarketSide  # string(BUY or SELL)\n        # Get API credentials for owner field\n        apiCredentials = self.get_api_credentials()\n        owner = self.safe_string(apiCredentials, 'apiKey')\n        if owner is None:\n            raise AuthenticationError(self.id + ' buildOrder() requires API credentials(apiKey)')\n        # Build request payload according to API specification\n        # Top-level fields: order, owner, orderType\n        requestPayload: dict = {\n            'order': signedOrder,\n            'owner': owner,\n            'orderType': orderType.upper(),\n        }\n        # Add optional parameters if provided\n        clientOrderId = self.safe_string(params, 'clientOrderId')\n        if clientOrderId is not None:\n            requestPayload['clientOrderId'] = clientOrderId\n        postOnly = self.safe_bool(params, 'postOnly', False)\n        if postOnly:\n            requestPayload['postOnly'] = True\n        return requestPayload\n\n    def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:\n        \"\"\"\n        create a trade order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/create-order\n        https://github.com/Polymarket/clob-order-utils\n        https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n\n        :param str symbol: unified symbol of the market to create an order in\n        :param str type: 'market' or 'limit'\n        :param str side: 'buy' or 'sell'\n        :param float amount: how much you want to trade in units of the base currency\n        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required if market has multiple outcomes)\n        :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK', 'GTD'(default: 'GTC')\n        :param str [params.clientOrderId]: a unique id for the order\n        :param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately\n        :param number [params.expiration]: expiration timestamp in seconds(default: 30 days from now)\n        :param number [params.nonce]: order nonce(default: current timestamp)\n        :param number [params.feeRateBps]: fee rate in basis points(default: fetched from API)\n        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Build the order request payload\n        requestPayload = self.build_order(symbol, type, side, amount, price, params)\n        # Extract clientOrderId from request payload for return value\n        clientOrderId = self.safe_string(requestPayload, 'clientOrderId')\n        # Submit order via POST /order endpoint\n        response = self.clob_private_post_order(self.extend(requestPayload, params))\n        # Response format:\n        # {\n        #     \"success\": boolean,\n        #     \"errorMsg\": string(if error),\n        #     \"orderId\": string,\n        #     \"orderHashes\": string[](if order was marketable)\n        # }\n        success = self.safe_bool(response, 'success', True)\n        if not success:\n            errorMsg = self.safe_string(response, 'errorMsg', 'Unknown error')\n            raise ExchangeError(self.id + ' createOrder() failed: ' + errorMsg)\n        orderId = self.safe_string(response, 'orderID')\n        if orderId is None:\n            raise ExchangeError(self.id + ' createOrder() response missing orderID')\n        market = None\n        if symbol:\n            market = self.market(symbol)\n        # Combine response with order details from requestPayload for parseOrder\n        orderData = self.extend({\n            'orderID': orderId,\n            'clientOrderId': clientOrderId,\n            'order': requestPayload['order'],  # Include the signed order for additional context\n            'order_type': requestPayload['orderType'],  # Include orderType for parseOrder\n        }, response)\n        order = self.parse_order(orderData, market)\n        return order\n\n    def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]:\n        \"\"\"\n        create multiple trade orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/create-order-batch\n\n        :param Array orders: list of orders to create, each order should contain the parameters required by createOrder\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: an array of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        orderRequests = []\n        clientOrderIds = []\n        symbols = []\n        for i in range(0, len(orders)):\n            order = orders[i]\n            symbol = self.safe_string(order, 'symbol')\n            if symbol is None:\n                raise ArgumentsRequired(self.id + ' createOrders() requires a symbol in each order')\n            type = self.safe_string(order, 'type')\n            side = self.safe_string(order, 'side')\n            amount = self.safe_number(order, 'amount')\n            price = self.safe_number(order, 'price')\n            orderParams = self.safe_dict(order, 'params', {})\n            # Merge order-level params with top-level params\n            mergedParams = self.extend({}, params, orderParams)\n            # Get token_id from order params, order directly, or it will be resolved in buildOrder\n            tokenId = self.safe_string(orderParams, 'token_id') or self.safe_string(order, 'token_id')\n            if tokenId is not None:\n                mergedParams['token_id'] = tokenId\n            # Get clientOrderId from order params or order directly\n            clientOrderId = self.safe_string(orderParams, 'clientOrderId') or self.safe_string(order, 'clientOrderId')\n            if clientOrderId is not None:\n                mergedParams['clientOrderId'] = clientOrderId\n            # Get timeInForce from order params or order directly\n            timeInForce = self.safe_string(orderParams, 'timeInForce') or self.safe_string(order, 'timeInForce')\n            if timeInForce is not None:\n                mergedParams['timeInForce'] = timeInForce\n            # Build the order request payload using the shared buildOrder function\n            orderRequest = self.build_order(symbol, type, side, amount, price, mergedParams)\n            # Store clientOrderId from request payload for response parsing\n            requestClientOrderId = self.safe_string(orderRequest, 'clientOrderId')\n            clientOrderIds.append(requestClientOrderId)\n            symbols.append(symbol)\n            orderRequests.append(orderRequest)\n        # Submit batch orders via POST /orders endpoint\n        response = self.clob_private_post_orders(self.extend({'orders': orderRequests}, params))\n        # Response format: array of order responses, each with:\n        # {\n        #     \"success\": boolean,\n        #     \"errorMsg\": string(if error),\n        #     \"orderId\": string,\n        #     \"orderHashes\": string[](if order was marketable)\n        # }\n        result = []\n        for i in range(0, len(response)):\n            orderResponse = response[i]\n            success = self.safe_bool(orderResponse, 'success', True)\n            if not success:\n                errorMsg = self.safe_string(orderResponse, 'errorMsg', 'Unknown error')\n                raise ExchangeError(self.id + ' createOrders() failed for order ' + i + ': ' + errorMsg)\n            orderId = self.safe_string(orderResponse, 'orderID')\n            if orderId is None:\n                raise ExchangeError(self.id + ' createOrders() response missing orderID for order ' + i)\n            market = None\n            if symbols[i]:\n                market = self.market(symbols[i])\n            # Combine response with order details from orderRequests for parseOrder\n            orderData = self.extend({\n                'orderID': orderId,\n                'clientOrderId': clientOrderIds[i],\n                'order': orderRequests[i]['order'],  # Include the signed order for additional context\n                'order_type': orderRequests[i]['orderType'],  # Include orderType for parseOrder\n            }, orderResponse)\n            result.append(self.parse_order(orderData, market))\n        return result\n\n    def create_market_order(self, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}):\n        \"\"\"\n        create a market order\n        :param str symbol: unified symbol of the market to create an order in\n        :param str side: 'buy' or 'sell'\n        :param float amount: how much you want to trade in units of the base currency\n        :param float [price]: ignored for market orders\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        # Use IOC by default for market orders(allows partial fills)\n        # Users can override with params.timeInForce = 'FOK' if they need Fill-Or-Kill behavior\n        return self.create_order(symbol, 'market', side, amount, price, self.extend(params, {'timeInForce': 'IOC'}))\n\n    def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order:\n        \"\"\"\n        cancels an open order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-order\n\n        :param str id: order id\n        :param str symbol: unified symbol of the market the order was made in\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Based on cancel() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL = \"/order\")\n        # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n        # See https://docs.polymarket.com/developers/CLOB/orders/cancel-order\n        response = self.clob_private_delete_order(self.extend({'order_id': id}, params))\n        canceled = self.safe_list(response, 'canceled', [])\n        notCanceled = self.safe_dict(response, 'not_canceled', {})\n        # Check if order was successfully canceled\n        isCanceled = False\n        for i in range(0, len(canceled)):\n            if canceled[i] == id:\n                isCanceled = True\n                break\n        if isCanceled:\n            # Order was canceled, parse order from response data\n            market = self.market(symbol) if symbol else None\n            orderData = {\n                'id': id,\n                'status': 'canceled',\n                'info': response,\n            }\n            return self.parse_order(orderData, market)\n        else:\n            # Check if order is in not_canceled map\n            reason = self.safe_string(notCanceled, id)\n            if reason is not None:\n                # Order couldn't be canceled, raise error with reason\n                raise ExchangeError(self.id + ' cancelOrder() failed: ' + reason)\n            else:\n                # Order ID not found in response(shouldn't happen)\n                raise ExchangeError(self.id + ' cancelOrder() unexpected response format')\n\n    def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]:\n        \"\"\"\n        cancel multiple orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-order-batch\n\n        :param str[] ids: order ids\n        :param str symbol: unified symbol of the market the orders were made in\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: an array of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Based on cancel_orders() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ORDERS = \"/orders\")\n        # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n        # See https://docs.polymarket.com/developers/CLOB/orders/cancel-order-batch\n        response = self.clob_private_delete_orders(self.extend({'order_ids': ids}, params))\n        canceled = self.safe_list(response, 'canceled', [])\n        notCanceled = self.safe_dict(response, 'not_canceled', {})\n        market = self.market(symbol) if symbol else None\n        orders: List[Order] = []\n        # Add canceled orders\n        for i in range(0, len(canceled)):\n            orderId = canceled[i]\n            orderData = {\n                'id': orderId,\n                'status': 'canceled',\n                'info': response,\n            }\n            orders.append(self.parse_order(orderData, market))\n        # Verify all requested orders are accounted for in the response\n        for i in range(0, len(ids)):\n            orderId = ids[i]\n            isInCanceled = False\n            for j in range(0, len(canceled)):\n                if canceled[j] == orderId:\n                    isInCanceled = True\n                    break\n            if not isInCanceled and not (orderId in notCanceled):\n                # Order ID not found in response(unexpected)\n                raise ExchangeError(self.id + ' cancelOrders() unexpected response format for order ' + orderId)\n        return orders\n\n    def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]:\n        \"\"\"\n        cancel all open orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-all-orders\n\n        :param str [symbol]: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        response\n        if symbol is not None:\n            # Use cancel-market-orders endpoint when symbol is provided\n            # See https://docs.polymarket.com/developers/CLOB/orders/cancel-market-orders\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            # Get condition_id(market ID)\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            # Get asset_id from clobTokenIds\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            request: dict = {}\n            if conditionId is not None:\n                request['market'] = conditionId\n            if len(clobTokenIds) > 0:\n                request['asset_id'] = clobTokenIds[0]\n            # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n            response = self.clob_private_delete_cancel_market_orders(self.extend(request, params))\n        else:\n            # Use cancel-all endpoint when symbol is None\n            # Based on cancel_all() from py-clob-client\n            # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ALL = \"/cancel-all\")\n            # Response format: {canceled: string[], not_canceled: {order_id -> reason}}\n            # See https://docs.polymarket.com/developers/CLOB/orders/cancel-all-orders\n            response = self.clob_private_delete_cancel_all(params)\n        canceled = self.safe_list(response, 'canceled', [])\n        orderMarket = self.market(symbol) if symbol else None\n        orders: List[Order] = []\n        # Add canceled orders\n        for i in range(0, len(canceled)):\n            orderId = canceled[i]\n            orderData = {\n                'id': orderId,\n                'status': 'canceled',\n                'info': response,\n            }\n            orders.append(self.parse_order(orderData, orderMarket))\n        return orders\n\n    def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order:\n        \"\"\"\n        fetches information on an order made by the user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/get-order\n\n        :param str id: order id\n        :param str symbol: unified symbol of the market the order was made in\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Based on get_order() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_ORDER = \"/data/order/\")\n        response = self.clob_private_get_order(self.extend({'order_id': id}, params))\n        market = self.market(symbol) if symbol else None\n        return self.parse_order(response, market)\n\n    def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:\n        \"\"\"\n        fetches information on multiple orders made by the user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/orders/get-orders\n\n        :param str symbol: unified symbol of the market the orders were made in\n        :param int [since]: the earliest time in ms to fetch orders for\n        :param int [limit]: the maximum number of order structures to retrieve\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: filter orders by order id\n        :param str [params.market]: filter orders by market id\n        :param str [params.asset_id]: filter orders by asset id(alias token_id)\n        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        self.load_markets()\n        self.ensure_api_credentials(params)\n        request = {}\n        if symbol is not None:\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            # Filter by condition_id(market) to get all orders for self market\n            # This is more appropriate than filtering by asset_id alone, market can have multiple outcomes\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            if conditionId is not None:\n                request['market'] = conditionId\n            # Also include asset_id for backward compatibility and more specific filtering\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                # The Polymarket L2 getOpenOrders() endpoint filters by asset_id\n                request['asset_id'] = clobTokenIds[0]\n                # Keep backward compatibility for legacy token_id usage\n                request['token_id'] = clobTokenIds[0]\n        id = self.safe_string(params, 'id')\n        if id is not None:\n            request['id'] = id\n        marketId = self.safe_string(params, 'market')\n        if marketId is not None:\n            request['market'] = marketId\n        assetId = self.safe_string_2(params, 'asset_id', 'token_id')\n        if assetId is not None:\n            request['asset_id'] = assetId\n            request['token_id'] = assetId\n        initialCursor = self.safe_string(self.options, 'initialCursor')\n        endCursor = self.safe_string(self.options, 'endCursor')\n        nextCursor = initialCursor\n        ordersResponse: List[Any] = []\n        while(True):\n            response = self.clob_private_get_orders(self.extend(request, {'next_cursor': nextCursor}, params))\n            data = self.safe_list(response, 'data', [])\n            ordersResponse = self.array_concat(ordersResponse, data)\n            if limit is not None and len(ordersResponse) >= limit:\n                break\n            nextCursor = self.safe_string(response, 'next_cursor')\n            if nextCursor is None or nextCursor == endCursor:\n                break\n        orderMarket = self.market(symbol) if symbol else None\n        return self.parse_orders(ordersResponse, orderMarket, since, limit)\n\n    def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:\n        \"\"\"\n        fetch all unfilled currently open orders\n        :param str symbol: unified symbol of the market to fetch open orders for\n        :param int [since]: the earliest time in ms to fetch open orders for\n        :param int [limit]: the maximum number of open order structures to retrieve\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        # The Polymarket getOpenOrders() endpoint already returns open orders\n        return self.fetch_orders(symbol, since, limit, params)\n\n    def parse_order(self, order: dict, market: Market = None) -> Order:\n        \"\"\"\n        parses an order from the exchange response format\n        :param dict order: order response from the exchange\n        :param dict [market]: market structure\n        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`\n        \"\"\"\n        # Handle createOrder/createOrders response format:\n        # {\n        #   \"success\": boolean,\n        #   \"errorMsg\": string(if error),\n        #   \"orderID\": string,\n        #   \"orderHashes\": string[](if order was marketable)\n        # }\n        # Or fetchOrder response format(OpenOrder interface):\n        # {\n        #   id: string\n        #   status: string\n        #   owner: string\n        #   maker_address: string\n        #   market: string\n        #   asset_id: string\n        #   side: string\n        #   original_size: string\n        #   size_matched: string\n        #   price: string\n        #   associate_trades: string[]\n        #   outcome: string\n        #   created_at: number  # seconds\n        #   expiration: string\n        #   order_type: string\n        # }\n        id = self.safe_string(order, 'id')\n        # Handle createOrder response format(has orderID instead of id)\n        if id is None:\n            id = self.safe_string(order, 'orderID')\n        marketId = self.safe_string(order, 'market')\n        assetId = self.safe_string(order, 'asset_id')\n        if market is None and marketId is not None:\n            market = self.safe_market(marketId, None)\n        symbol = None\n        if market is not None and market['symbol'] is not None:\n            symbol = market['symbol']\n        elif assetId is not None:\n            symbol = assetId\n        # Handle createOrder response - get side from order object if available\n        sideStr = self.safe_string_lower(order, 'side')\n        # If side is not in order, try to get it from the order object passed in createOrder\n        if sideStr is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                sideStr = self.safe_string_lower(orderObj, 'side')\n        side = sideStr if (sideStr == 'buy' or sideStr == 'sell') else None\n        orderType = self.safe_string(order, 'order_type')\n        # Handle createOrder response - get orderType from order object if available\n        if orderType is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                orderType = self.safe_string(orderObj, 'orderType')\n            # Also check at top level(from requestPayload)\n            if orderType is None:\n                orderType = self.safe_string(order, 'orderType')\n        # Normalize orderType to lowercase for consistent parsing\n        if orderType is not None:\n            orderType = orderType.lower()\n        # Amounts\n        amount = self.safe_number(order, 'original_size')\n        # Handle createOrder response - get amount from order object if available\n        if amount is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                amount = self.safe_number(orderObj, 'size')\n        filled = self.safe_number(order, 'size_matched')\n        remaining = self.safe_number(order, 'remaining_size')\n        if remaining is None and amount is not None and filled is not None:\n            remaining = amount - filled\n        # Price\n        price = self.safe_number(order, 'price')\n        # Handle createOrder response - get price from order object if available\n        if price is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                price = self.safe_number(orderObj, 'price')\n        # Status\n        statusStr = self.safe_string(order, 'status', '')\n        status = self.parse_order_status(statusStr)\n        # Timestamps(created_at is seconds)\n        createdAt = self.safe_integer(order, 'created_at')\n        timestamp = createdAt * 1000 if (createdAt is not None) else None\n        # Get clientOrderId from order or from the order object\n        clientOrderId = self.safe_string(order, 'clientOrderId')\n        if clientOrderId is None:\n            orderObj = self.safe_dict(order, 'order')\n            if orderObj is not None:\n                clientOrderId = self.safe_string(orderObj, 'clientOrderId')\n        # No explicit updated_at in interface; leave lastTradeTimestamp None\n        return self.safe_order({\n            'id': id,\n            'clientOrderId': clientOrderId,\n            'info': order,\n            'timestamp': timestamp,\n            'datetime': self.iso8601(timestamp) if timestamp else None,\n            'lastTradeTimestamp': None,\n            'status': status,\n            'symbol': symbol,\n            'type': self.parse_order_type(orderType),\n            'timeInForce': self.parse_time_in_force(orderType),\n            'side': side,\n            'price': price,\n            'amount': amount,\n            'cost': None,\n            'average': None,\n            'filled': filled,\n            'remaining': remaining,\n            'fee': None,\n        }, market)\n\n    def parse_order_status(self, status: Str) -> Str:\n        \"\"\"\n        parse the status of an order\n        :param str status: order status from exchange\n        :returns str: a unified order status\n        \"\"\"\n        if status is None or status == '':\n            return 'open'  # Default to 'open' if no status is provided\n        statuses: dict = {\n            # https://docs.polymarket.com/developers/CLOB/orders/create-order#status\n            'matched': 'closed',   # order placed and matched with an existing resting order\n            'live': 'open',         # order placed and resting on the book\n            'delayed': 'open',      # order marketable, but subject to matching delay\n            'unmatched': 'open',    # order marketable, but failure delaying, placement successful\n            'canceled': 'canceled',  # CCXT unified status for canceled orders\n        }\n        normalizedStatus = status.lower()\n        return self.safe_string(statuses, normalizedStatus, normalizedStatus)\n\n    def parse_order_type(self, type: Str) -> Str:\n        types: dict = {\n            'fok': 'market',  # Fill-Or-Kill: market order\n            'fak': 'market',  # Fill-And-Kill: market order\n            'ioc': 'market',  # Immediate-Or-Cancel: market order\n            'gtc': 'limit',  # Good-Til-Cancelled: limit order\n            'gtd': 'limit',  # Good-Til-Date: limit order\n        }\n        return self.safe_string(types, type, 'limit')\n\n    def parse_time_in_force(self, timeInForce: Str) -> Str:\n        if timeInForce is None:\n            return None\n        timeInForces: dict = {\n            'fok': 'FOK',  # Fill-Or-Kill\n            'fak': 'FAK',  # Fill-And-Kill\n            'ioc': 'IOC',  # Immediate-Or-Cancel\n            'gtc': 'GTC',  # Good-Til-Cancelled\n            'gtd': 'GTD',  # Good-Til-Date\n        }\n        normalized = timeInForce.lower()\n        mapped = self.safe_string(timeInForces, normalized)\n        return mapped is not mapped if None else timeInForce.upper()\n\n    def fetch_time(self, params={}) -> Int:\n        \"\"\"\n        fetches the current integer timestamp in milliseconds from the exchange server\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L178\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns int: the current integer timestamp in milliseconds from the exchange server\n        \"\"\"\n        # Based on get_server_time() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L178\n        response = self.clob_public_get_time(params)\n        # Response format: timestamp in seconds(Unix timestamp)\n        # Convert to milliseconds for CCXT standard\n        timestamp = self.safe_integer(response, 'timestamp')\n        if timestamp is not None:\n            return timestamp * 1000  # Convert seconds to milliseconds\n        # Fallback: if response is just a number\n        if isinstance(response, numbers.Real):\n            return response * 1000\n        # Fallback: use current time if server time not available\n        return self.milliseconds()\n\n    def fetch_status(self, params={}):\n        \"\"\"\n        the latest known information on the availability of the exchange API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: a `status structure <https://docs.ccxt.com/#/?id=exchange-status-structure>`\n        \"\"\"\n        # Based on get_ok() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n        try:\n            self.clob_public_get_ok(params)\n            return {\n                'status': 'ok',\n                'updated': None,\n                'eta': None,\n                'url': None,\n            }\n        except Exception as e:\n            return {\n                'status': 'error',\n                'updated': None,\n                'eta': None,\n                'url': None,\n            }\n\n    def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface:\n        \"\"\"\n        fetches the trading fee for a market\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param str symbol: unified symbol of the market to fetch the fee for\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required if not in market info)\n        :returns dict: a `fee structure <https://docs.ccxt.com/#/?id=fee-structure>`\n        \"\"\"\n        self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        # Get token ID from params or market info\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            clobTokenIds = self.safe_value(marketInfo, 'clobTokenIds', [])\n            if len(clobTokenIds) > 0:\n                tokenId = clobTokenIds[0]\n            else:\n                raise ArgumentsRequired(self.id + ' fetchTradingFee() requires a token_id parameter for market ' + symbol)\n        # Based on get_fee_rate() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        response = self.clob_public_get_fee_rate(self.extend({'token_id': tokenId}, params))\n        # Response format: {\"fee_rate\": \"0.02\"} or {\"fee_rate_bps\": 200}(basis points)\n        feeRate = self.safe_string(response, 'fee_rate')\n        feeRateBps = self.safe_integer(response, 'fee_rate_bps')\n        maker: Num = None\n        taker: Num = None\n        if feeRate is not None:\n            fee = self.parse_number(feeRate)\n            maker = fee\n            taker = fee\n        elif feeRateBps is not None:\n            # Convert basis points to percentage(200 bps = 2% = 0.02)\n            fee = self.parse_number(feeRateBps) / 10000\n            maker = fee\n            taker = fee\n        else:\n            # Default fee from describe() if not available\n            maker = self.safe_number(self.fees['trading'], 'maker')\n            taker = self.safe_number(self.fees['trading'], 'taker')\n        # Ensure we have valid numbers(fallback to default if None)\n        makerFee: Num = maker is not maker if None else self.parse_number('0.02')\n        takerFee: Num = taker is not taker if None else self.parse_number('0.02')\n        result: TradingFeeInterface = {\n            'info': response,\n            'symbol': symbol,\n            'maker': makerFee,\n            'taker': takerFee,\n            'percentage': True,\n            'tierBased': False,\n        }\n        return result\n\n    def fetch_open_interest(self, symbol: str, params={}):\n        \"\"\"\n        retrieves the open interest of a market\n\n        https://docs.polymarket.com/api-reference/misc/get-open-interest\n\n        :param str symbol: unified CCXT market symbol\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure:\n        \"\"\"\n        self.load_markets()\n        market = self.market(symbol)\n        marketInfo = self.safe_dict(market, 'info', {})\n        conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n        # API expects market of condition IDs\n        request: dict = {\n            'market': [conditionId],\n        }\n        response = self.data_public_get_open_interest(self.extend(request, params))\n        return self.parse_open_interest(response, market)\n\n    def parse_open_interest(self, interest: dict, market: Market = None):\n        \"\"\"\n        parses open interest data from the exchange response format\n        :param dict interest: open interest data from the exchange\n        :param dict [market]: the market self open interest is for\n        :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure:\n        \"\"\"\n        # Polymarket Data API /oi response format\n        # Response is an array of objects with market(condition ID) and value\n        # Example response structure:\n        # [\n        #   {\n        #     \"market\": \"0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917\",\n        #     \"value\": 123\n        #   }\n        # ]\n        timestamp = self.milliseconds()\n        # Handle array response\n        openInterestData: dict = {}\n        if isinstance(interest, list):\n            # For single symbol query, get the first item\n            if len(interest) > 0:\n                openInterestData = interest[0]\n        elif isinstance(interest, dict) and interest != None:\n            # Fallback: handle object response if API changes\n            openInterestData = interest\n        # Extract open interest value from the response\n        # API returns \"value\" field which represents the open interest value\n        openInterestValue = self.safe_number(openInterestData, 'value', 0)\n        # For Polymarket, value is typically in USDC, so we use it amount and value\n        # If we need to distinguish, we could parse additional fields if available\n        return self.safe_open_interest({\n            'symbol': market['symbol'] if market else None,\n            'openInterestAmount': openInterestValue,  # Using value since API only provides value\n            'openInterestValue': openInterestValue,\n            'timestamp': timestamp,\n            'datetime': self.iso8601(timestamp),\n            'info': interest,\n        }, market)\n\n    def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        fetch all trades made by the user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param str symbol: unified symbol of the market to fetch trades for\n        :param int [since]: the earliest time in ms to fetch trades for\n        :param int [limit]: the maximum number of trades structures to retrieve\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: filter trades by market(condition_id)\n        :param str [params.asset_id]: filter trades by asset ID\n        :param str [params.id]: filter by trade id\n        :param str [params.maker_address]: filter by maker address\n        :param str [params.before]: pagination cursor(see API docs)\n        :param str [params.after]: pagination cursor(see API docs)\n        :param str [params.next_cursor]: pagination cursor(default: \"MA==\")\n        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        request: dict = {}\n        market = None\n        if symbol is not None:\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            # Filter by condition_id(market) to get all trades for self market\n            # Don't automatically add asset_id filter would restrict to only one outcome\n            conditionId = self.safe_string(marketInfo, 'condition_id', self.safe_string(market, 'id'))\n            if conditionId is not None:\n                request['market'] = conditionId\n        # Backward compatibility: token_id alias to asset_id\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is not None:\n            request['asset_id'] = tokenId\n        marketId = self.safe_string(params, 'market')\n        if marketId is not None:\n            request['market'] = marketId\n        assetId = self.safe_string_2(params, 'asset_id', 'assetId')\n        if assetId is not None:\n            request['asset_id'] = assetId\n        id = self.safe_string(params, 'id')\n        if id is not None:\n            request['id'] = id\n        makerAddress = self.safe_string_2(params, 'maker_address', 'makerAddress')\n        if makerAddress is not None:\n            request['maker_address'] = makerAddress\n        before = self.safe_string(params, 'before')\n        if before is not None:\n            request['before'] = before\n        after = self.safe_string(params, 'after')\n        if after is not None:\n            request['after'] = after\n        if since is not None:\n            # Map ccxt since to Polymarket's \"after\" cursor using seconds\n            request['after'] = self.number_to_string(int(math.floor(since / 1000)))\n        if limit is not None:\n            request['limit'] = limit\n        results: List[Any] = []\n        initialCursor = self.safe_string(self.options, 'initialCursor')\n        endCursor = self.safe_string(self.options, 'endCursor')\n        next_cursor = initialCursor\n        while(next_cursor != endCursor):\n            response = self.clob_private_get_trades(self.extend(request, {'next_cursor': next_cursor}, params))\n            next_cursor = self.safe_string(response, 'next_cursor', endCursor)\n            data = self.safe_list(response, 'data', []) or []\n            results = self.array_concat(results, data)\n            if limit is not None and len(results) >= limit:\n                break\n        return self.parse_trades(results, market, since, limit)\n\n    def fetch_user_trades(self, user: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:\n        \"\"\"\n        fetch trades for a specific user\n\n        https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n\n        :param str user: user address(0x-prefixed, 40 hex chars)\n        :param str [symbol]: unified symbol of the market to fetch trades for\n        :param int [since]: timestamp in ms of the earliest trade to fetch\n        :param int [limit]: the maximum amount of trades to fetch(default: 100, max: 10000)\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.offset]: offset for pagination(default: 0, max: 10000)\n        :param boolean [params.takerOnly]: if True, returns only trades where the user is the taker(default: True)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :param str[] [params.market]: comma-separated list of condition IDs(mutually exclusive with symbol)\n        :param int[] [params.eventId]: comma-separated list of event IDs(mutually exclusive with market)\n        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`\n        \"\"\"\n        self.load_markets()\n        request: dict = {\n            'user': user,\n        }\n        market = None\n        if symbol is not None:\n            market = self.market(symbol)\n            marketInfo = self.safe_dict(market, 'info', {})\n            conditionId = self.safe_string(marketInfo, 'condition_id', market['id'])\n            request['market'] = [conditionId]\n        marketParam = self.safe_value(params, 'market')\n        if marketParam is not None:\n            # Convert to array if it's a string or single value\n            if isinstance(marketParam, list):\n                request['market'] = marketParam\n            else:\n                request['market'] = [marketParam]\n        eventId = self.safe_value(params, 'eventId')\n        if eventId is not None:\n            if isinstance(eventId, list):\n                request['eventId'] = eventId\n            else:\n                request['eventId'] = [eventId]\n        if limit is not None:\n            request['limit'] = min(limit, 10000)  # Cap at max 10000\n        offset = self.safe_integer(params, 'offset')\n        if offset is not None:\n            request['offset'] = offset\n        takerOnly = self.safe_bool(params, 'takerOnly', True)\n        request['takerOnly'] = takerOnly\n        side = self.safe_string_upper(params, 'side')\n        if side is not None:\n            request['side'] = side\n        response = self.data_public_get_trades(self.extend(request, self.omit(params, ['market', 'eventId', 'offset', 'takerOnly', 'side'])))\n        tradesData = []\n        if isinstance(response, list):\n            tradesData = response\n        else:\n            dataList = self.safe_list(response, 'data', [])\n            if dataList is not None:\n                tradesData = dataList\n        return self.parse_trades(tradesData, market, since, limit)\n\n    def fetch_balance(self, params={}):\n        \"\"\"\n        fetches balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/clients/methods-l2#getbalanceallowance\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_type]: asset type: 'COLLATERAL'(default) or 'CONDITIONAL'\n        :param str [params.token_id]: token ID, default: from options.defaultTokenId)\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Default asset_type to COLLATERAL if not provided\n        assetType = self.safe_string(params, 'asset_type', 'COLLATERAL')\n        params['asset_type'] = assetType\n        # Use signature_type from params or fall back to options\n        signatureType = self.get_signature_type(params)\n        request: dict = {\n            'asset_type': assetType,\n        }\n        if signatureType is not None:\n            request['signature_type'] = signatureType\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            defaultTokenId = self.safe_string(self.options, 'defaultTokenId')\n            if defaultTokenId is not None:\n                request['token_id'] = defaultTokenId\n        else:\n            request['token_id'] = tokenId\n        # Fetch balance and allowance from CLOB endpoint\n        clobResponse = self.clob_private_get_balance_allowance(request)\n        #\n        #     {\n        #         \"balance\": \"1000000\",\n        #         \"allowance\": \"0\"\n        #     }\n        #\n        balance = self.safe_string(clobResponse, 'balance')\n        allowance = self.safe_string(clobResponse, 'allowance')\n        collateral = self.safe_string(self.options, 'defaultCollateral', 'USDC')\n        # Convert CLOB balance and allowance(6 decimals) to standard units\n        collateralTotalValue = None\n        collateralUsedValue = None\n        collateralFreeValue = None\n        if balance is not None:\n            parsedBalance = self.parse_number(balance)\n            if parsedBalance is not None:\n                collateralTotalValue = parsedBalance / 1000000\n        if allowance is not None:\n            parsedAllowance = self.parse_number(allowance)\n            if parsedAllowance is not None:\n                collateralUsedValue = parsedAllowance / 1000000\n        # Calculate free balance: total - used(allowance)\n        if collateralTotalValue is not None and collateralUsedValue is not None:\n            collateralFreeValue = collateralTotalValue - collateralUsedValue\n        elif collateralTotalValue is not None:\n            collateralFreeValue = collateralTotalValue\n        result: dict = {\n            'info': clobResponse,\n        }\n        if collateralTotalValue is not None:\n            account = self.account()\n            account['total'] = collateralTotalValue\n            if collateralFreeValue is not None:\n                account['free'] = collateralFreeValue\n            if collateralUsedValue is not None:\n                account['used'] = collateralUsedValue\n            result[collateral] = account\n        return self.safe_balance(result)\n\n    def get_notifications(self, params={}):\n        \"\"\"\n        fetches notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Use signature_type from params or fall back to options\n        signatureType = self.get_signature_type(params)\n        request: dict = {}\n        if signatureType is not None:\n            request['signature_type'] = signatureType\n        # Based on get_notifications() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        response = self.clob_private_get_notifications(self.extend(request, params))\n        return response\n\n    def drop_notifications(self, params={}):\n        \"\"\"\n        drops notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.notification_id]: specific notification ID to drop(optional)\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Use signature_type from params or fall back to options\n        signatureType = self.get_signature_type(params)\n        request: dict = {}\n        if signatureType is not None:\n            request['signature_type'] = signatureType\n        # Based on drop_notifications() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        response = self.clob_private_delete_notifications(self.extend(request, params))\n        return response\n\n    def get_balance_allowance(self, params={}):\n        \"\"\"\n        fetches balance and allowance for the authenticated user(alias for fetchBalance)\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/clients/methods-l2#getbalanceallowance\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.asset_type]: asset type: 'COLLATERAL'(default) or 'CONDITIONAL'\n        :param str [params.token_id]: token ID, default: from options.defaultTokenId)\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Alias for fetchBalance, but returns raw response\n        # Use signature_type from params or fall back to options\n        if self.safe_integer(params, 'signature_type') is None:\n            signatureType = self.get_signature_type(params)\n            if signatureType is not None:\n                params['signature_type'] = signatureType\n        # Default asset_type to COLLATERAL if not provided(for USDC balance)\n        assetType = self.safe_string(params, 'asset_type', 'COLLATERAL')\n        params['asset_type'] = assetType\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            defaultTokenId = self.safe_string(self.options, 'defaultTokenId')\n            if defaultTokenId is not None:\n                params['token_id'] = defaultTokenId\n        else:\n            params['token_id'] = tokenId\n        return self.clob_private_get_balance_allowance(params)\n\n    def update_balance_allowance(self, params={}):\n        \"\"\"\n        updates balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        # Based on update_balance_allowance() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        # Use signature_type from params or fall back to options\n        if self.safe_integer(params, 'signature_type') is None:\n            signatureType = self.get_signature_type(params)\n            if signatureType is not None:\n                params['signature_type'] = signatureType\n        response = self.clob_private_put_balance_allowance(params)\n        return response\n\n    def is_order_scoring(self, params={}):\n        \"\"\"\n        checks if an order is currently scoring\n\n        https://docs.polymarket.com/developers/CLOB/orders/check-scoring#check-order-reward-scoring\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID to check(required)\n        :returns dict: response from the exchange indicating if order is scoring\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        orderId = self.safe_string(params, 'order_id')\n        if orderId is None:\n            raise ArgumentsRequired(self.id + ' isOrderScoring() requires an order_id parameter')\n        response = self.clob_private_get_is_order_scoring(params)\n        # Response: {scoring: boolean}\n        return response\n\n    def are_orders_scoring(self, params={}):\n        \"\"\"\n        checks if multiple orders are currently scoring\n\n        https://docs.polymarket.com/developers/CLOB/orders/check-scoring#check-order-reward-scoring\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.order_ids]: array of order IDs to check(required)\n        :returns dict: response from the exchange indicating which orders are scoring\n        \"\"\"\n        self.load_markets()\n        # Ensure API credentials are generated(lazy generation)\n        self.ensure_api_credentials(params)\n        orderIds = self.safe_value_2(params, 'order_ids', 'orderIds')\n        if orderIds is None or not isinstance(orderIds, list):\n            raise ArgumentsRequired(self.id + ' areOrdersScoring() requires an order_ids parameter(array of order IDs)')\n        response = self.clob_private_post_are_orders_scoring(self.extend({'orderIds': orderIds}, params))\n        # Response: {orderId: boolean, ...}\n        return response\n\n    def clob_public_get_markets(self, params={}):\n        \"\"\"\n        fetches markets from CLOB API(matches clob-client getMarkets())\n\n        https://github.com/Polymarket/clob-client/blob/main/src/client.ts\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.next_cursor]: pagination cursor(default: options.initialCursor)\n        :returns dict: response from the exchange\n        \"\"\"\n        # Pass api ['clob', 'public'] to match the expected format\n        # The api parameter should be an array [api_type, access_level] for exchanges with multiple API types\n        return self.request('markets', ['clob', 'public'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def gamma_public_get_markets(self, params={}):\n        \"\"\"\n        fetches markets from Gamma API\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # Pass api ['gamma', 'public'] to match the expected format\n        # The api parameter should be an array [api_type, access_level] for exchanges with multiple API types\n        return self.request('markets', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    def gamma_public_get_markets_id(self, params={}):\n        \"\"\"\n        fetches a specific market by ID from Gamma API\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetMarketsId() requires an id parameter')\n        path = 'markets/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def gamma_public_get_markets_id_tags(self, params={}):\n        \"\"\"\n        fetches tags for a specific market by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the market ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetMarketsIdTags() requires an id parameter')\n        path = 'markets/' + self.encode_uri_component(id) + '/tags'\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def gamma_public_get_markets_slug_slug(self, params={}):\n        \"\"\"\n        fetches a specific market by slug from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.slug]: the market slug(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        slug = self.safe_string(params, 'slug')\n        if slug is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetMarketsSlugSlug() requires a slug parameter')\n        path = 'markets/slug/' + self.encode_uri_component(slug)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'slug'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def gamma_public_get_events(self, params={}):\n        \"\"\"\n        fetches events from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :param str [params.category]: filter by category\n        :param str [params.slug]: filter by slug\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('events', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    def gamma_public_get_events_id(self, params={}):\n        \"\"\"\n        fetches a specific event by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the event ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetEventsId() requires an id parameter')\n        path = 'events/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def gamma_public_get_series(self, params={}):\n        \"\"\"\n        fetches series from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :param str [params.category]: filter by category\n        :param str [params.slug]: filter by slug\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('series', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    def gamma_public_get_series_id(self, params={}):\n        \"\"\"\n        fetches a specific series by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the series ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetSeriesId() requires an id parameter')\n        path = 'series/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def gamma_public_get_search(self, params={}):\n        \"\"\"\n        performs a full-text search across events, tags, and user profiles from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.q]: search query(required)\n        :param str [params.type]: filter by type: 'event', 'tag', 'user', etc.\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        q = self.safe_string(params, 'q')\n        if q is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetSearch() requires a q(query) parameter')\n        return self.request('search', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    def gamma_public_get_comments(self, params={}):\n        \"\"\"\n        fetches comments from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.event_id]: filter by event ID\n        :param str [params.series_id]: filter by series ID\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('comments', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    def gamma_public_get_comments_id(self, params={}):\n        \"\"\"\n        fetches a specific comment by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the comment ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetCommentsId() requires an id parameter')\n        path = 'comments/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def gamma_public_get_sports(self, params={}):\n        \"\"\"\n        fetches sports data from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.league]: filter by league\n        :param str [params.team]: filter by team\n        :param int [params.limit]: maximum number of results to return\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('sports', ['gamma', 'public'], 'GET', self.extend({'api_type': 'gamma'}, params))\n\n    def gamma_public_get_sports_id(self, params={}):\n        \"\"\"\n        fetches a specific sport/team by ID from Gamma API\n\n        https://docs.polymarket.com/developers/gamma-markets-api/fetch-markets-guide\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.id]: the sport/team ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        id = self.safe_string(params, 'id')\n        if id is None:\n            raise ArgumentsRequired(self.id + ' gammaPublicGetSportsId() requires an id parameter')\n        path = 'sports/' + self.encode_uri_component(id)\n        remainingParams = self.extend({'api_type': 'gamma'}, self.omit(params, 'id'))\n        return self.request(path, ['gamma', 'public'], 'GET', remainingParams)\n\n    def data_public_get_positions(self, params={}):\n        \"\"\"\n        fetches current positions for a user from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :param str[] [params.market]: comma-separated list of condition IDs(mutually exclusive with eventId)\n        :param int[] [params.eventId]: comma-separated list of event IDs(mutually exclusive with market)\n        :param number [params.sizeThreshold]: minimum size threshold(default: 1)\n        :param boolean [params.redeemable]: filter by redeemable positions(default: False)\n        :param boolean [params.mergeable]: filter by mergeable positions(default: False)\n        :param int [params.limit]: maximum number of results(default: 100, max: 500)\n        :param int [params.offset]: offset for pagination(default: 0, max: 10000)\n        :param str [params.sortBy]: sort field: CURRENT, INITIAL, TOKENS, CASHPNL, PERCENTPNL, TITLE, RESOLVING, PRICE, AVGPRICE(default: TOKENS)\n        :param str [params.sortDirection]: sort direction: ASC, DESC(default: DESC)\n        :param str [params.title]: filter by title(max length: 100)\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetPositions() requires a user parameter')\n        return self.request('positions', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_trades(self, params={}):\n        \"\"\"\n        fetches trades for a user or markets from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-trades-for-a-user-or-markets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(optional, filter by user)\n        :param str[] [params.market]: comma-separated list of condition IDs(optional, filter by markets)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('trades', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_activity(self, params={}):\n        \"\"\"\n        fetches user activity from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-user-activity\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetActivity() requires a user parameter')\n        return self.request('activity', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_holders(self, params={}):\n        \"\"\"\n        fetches top holders for markets from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-top-holders-for-markets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.market]: comma-separated list of condition IDs(required)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_string(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetHolders() requires a market parameter')\n        return self.request('holders', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_total_value(self, params={}):\n        \"\"\"\n        fetches total value of a user's positions from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-total-value-of-a-users-positions\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetTotalValue() requires a user parameter')\n        return self.request('value', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_closed_positions(self, params={}):\n        \"\"\"\n        fetches closed positions for a user from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-closed-positions-for-a-user\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :param str[] [params.market]: comma-separated list of condition IDs(mutually exclusive with eventId)\n        :param int[] [params.eventId]: comma-separated list of event IDs(mutually exclusive with market)\n        :param int [params.limit]: maximum number of results\n        :param int [params.offset]: offset for pagination\n        :param str [params.sortBy]: sort field\n        :param str [params.sortDirection]: sort direction: ASC, DESC\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetClosedPositions() requires a user parameter')\n        return self.request('closed-positions', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_traded(self, params={}):\n        \"\"\"\n        fetches total markets a user has traded from Data-API\n\n        https://docs.polymarket.com/api-reference/misc/get-total-markets-a-user-has-traded\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.user]: user address(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        user = self.safe_string(params, 'user')\n        if user is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetTraded() requires a user parameter')\n        return self.request('traded', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def data_public_get_open_interest(self, params={}):\n        \"\"\"\n        fetches open interest from Data-API\n\n        https://docs.polymarket.com/api-reference/misc/get-open-interest\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.market]: array of condition IDs(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_value(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetOpenInterest() requires a market parameter')\n        # Convert market to array if it's a single string\n        marketArray: List[str] = []\n        if isinstance(market, list):\n            marketArray = market\n        elif isinstance(market, str):\n            marketArray = [market]\n        else:\n            raise ArgumentsRequired(self.id + ' dataPublicGetOpenInterest() requires market to be a string or array of condition IDs')\n        # API expects market in query params\n        requestParams = self.extend({'market': marketArray}, self.omit(params, 'market'))\n        return self.request('oi', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, requestParams))\n\n    def data_public_get_live_volume(self, params={}):\n        \"\"\"\n        fetches live volume for an event from Data-API\n\n        https://docs.polymarket.com/api-reference/misc/get-live-volume-for-an-event\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.eventId]: event ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        eventId = self.safe_integer(params, 'eventId')\n        if eventId is None:\n            raise ArgumentsRequired(self.id + ' dataPublicGetLiveVolume() requires an eventId parameter')\n        return self.request('live-volume', ['data', 'public'], 'GET', self.extend({'api_type': 'data'}, params))\n\n    def bridge_public_get_supported_assets(self, params={}):\n        \"\"\"\n        fetches supported assets for bridging from Bridge API\n\n        https://docs.polymarket.com/developers/misc-endpoints/bridge-supported-assets\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('supported-assets', ['bridge', 'public'], 'GET', self.extend({'api_type': 'bridge'}, params))\n\n    def bridge_public_post_deposit(self, params={}):\n        \"\"\"\n        creates deposit addresses for bridging assets to Polymarket\n\n        https://docs.polymarket.com/developers/misc-endpoints/bridge-deposit\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.address]: Polymarket wallet address(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        address = self.safe_string(params, 'address')\n        if address is None:\n            raise ArgumentsRequired(self.id + ' bridgePublicPostDeposit() requires an address parameter')\n        body = self.json({'address': address})\n        remainingParams = self.extend({'api_type': 'bridge'}, self.omit(params, 'address'))\n        return self.request('deposit', ['bridge', 'public'], 'POST', remainingParams, None, body)\n\n    def create_deposit_address(self, code: str, params={}):\n        \"\"\"\n        create a deposit address for bridging assets to Polymarket\n\n        https://docs.polymarket.com/developers/misc-endpoints/bridge-deposit\n\n        :param str code: unified currency code\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.address]: Polymarket wallet address(required if not set in options)\n        :returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`\n        \"\"\"\n        # Get address from params or use default from options\n        address = self.safe_string(params, 'address')\n        if address is None:\n            # Try to get from options or raise error\n            address = self.safe_string(self.options, 'address')\n            if address is None:\n                raise ArgumentsRequired(self.id + ' createDepositAddress() requires an address parameter or address in options')\n        response = self.bridge_public_post_deposit(self.extend({'address': address}, params))\n        # Response format: {address: \"...\", depositAddresses: [{chainId, chainName, tokenAddress, tokenSymbol, depositAddress}, ...]}\n        depositAddresses = self.safe_list(response, 'depositAddresses', [])\n        # Find the deposit address for the requested currency code\n        # For Polymarket, all deposits are converted to USDC.e, but we can filter by tokenSymbol\n        currency = self.currency(code)\n        depositAddress = None\n        for i in range(0, len(depositAddresses)):\n            addr = depositAddresses[i]\n            tokenSymbol = self.safe_string(addr, 'tokenSymbol')\n            if tokenSymbol and tokenSymbol.upper() == currency['code'].upper():\n                depositAddress = self.safe_string(addr, 'depositAddress')\n                break\n        # If not found, return the first deposit address(default to USDC)\n        if depositAddress is None and len(depositAddresses) > 0:\n            depositAddress = self.safe_string(depositAddresses[0], 'depositAddress')\n        return {\n            'currency': code,\n            'address': depositAddress,\n            'tag': None,\n            'info': response,\n        }\n\n    def clob_public_get_orderbook_token_id(self, params={}):\n        \"\"\"\n        fetches orderbook for a specific token ID from CLOB API\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetOrderbookTokenId() requires a token_id parameter')\n        # Note: CLOB API uses /book endpoint with token_id parameter, not /orderbook/{token_id}\n        # See https://docs.polymarket.com/developers/CLOB/prices-books/get-book\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('book', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_post_books(self, params={}):\n        \"\"\"\n        fetches order books for multiple token IDs from CLOB API\n\n        https://docs.polymarket.com/api-reference/orderbook/get-multiple-order-books-summaries-by-request\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param Array [params.requests]: array of {token_id, limit?} objects(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        requests = self.safe_value(params, 'requests')\n        if requests is None or not isinstance(requests, list) or len(requests) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicPostBooks() requires a requests parameter(array of {token_id, limit?} objects)')\n        # Note: REST API endpoint format: POST /books with JSON body\n        # See https://docs.polymarket.com/api-reference/orderbook/get-multiple-order-books-summaries-by-request\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\", limit: 10}, ...]\n        # Response format: array of order book objects, each with asset_id, bids, asks, etc.\n        body = self.json(requests)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'requests'))\n        return self.request('books', ['clob', 'public'], 'POST', remainingParams, None, body)\n\n    def clob_public_get_market_trades_events(self, params={}):\n        \"\"\"\n        fetches market trade events for a specific condition ID from CLOB API\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getmarkettradesevents\n        https://docs.polymarket.com/developers/CLOB/trades/trades-data-api\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.condition_id]: the condition ID(market ID) for the market\n        :param int [params.limit]: the maximum number of trades to fetch(default: 100, max: 500)\n        :param int [params.offset]: number of trades to skip before starting to return results(default: 0)\n        :param boolean [params.takerOnly]: if True, returns only trades where the user is the taker(default: True)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :returns dict: response from the exchange\n        \"\"\"\n        conditionId = self.safe_string(params, 'condition_id')\n        if conditionId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetMarketTradesEvents() requires a condition_id parameter')\n        # Note: CLOB REST API endpoint format: /trades?market={condition_id}\n        # See https://docs.polymarket.com/developers/CLOB/trades/trades-data-api\n        # The client SDK method getMarketTradesEvents() uses a different endpoint, but the REST API uses /trades\n        request: dict = {\n            'market': conditionId,\n        }\n        remainingParams = self.omit(params, 'condition_id')\n        # Add optional parameters\n        limit = self.safe_integer(remainingParams, 'limit')\n        if limit is not None:\n            request['limit'] = limit\n        offset = self.safe_integer(remainingParams, 'offset')\n        if offset is not None:\n            request['offset'] = offset\n        takerOnly = self.safe_bool(remainingParams, 'takerOnly')\n        if takerOnly is not None:\n            request['takerOnly'] = takerOnly\n        side = self.safe_string(remainingParams, 'side')\n        if side is not None:\n            request['side'] = side\n        # Add any other remaining params\n        finalParams = self.extend({'api_type': 'clob'}, self.extend(request, self.omit(remainingParams, ['limit', 'offset', 'takerOnly', 'side'])))\n        return self.request('trades', ['clob', 'public'], 'GET', finalParams)\n\n    def clob_public_get_prices_history(self, params={}):\n        \"\"\"\n        fetches historical price data for a token from CLOB API\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#getpriceshistory\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: the token ID(market parameter)\n        :param str [params.interval]: the time interval: \"max\", \"1w\", \"1d\", \"6h\", \"1h\"\n        :param int [params.startTs]: timestamp in seconds of the earliest candle to fetch\n        :param int [params.endTs]: timestamp in seconds of the latest candle to fetch\n        :param number [params.fidelity]: data fidelity/quality\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_string(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetPricesHistory() requires a market(token_id) parameter')\n        # Note: REST API endpoint format: /prices-history\n        # See https://docs.polymarket.com/developers/CLOB/timeseries\n        # Required: market\n        # Time component(mutually exclusive): either(startTs and endTs) OR interval\n        # Optional: fidelity\n        # Response format: {\"history\": [{\"t\": timestamp, \"p\": price}, ...]}\n        request: dict = {\n            'market': market,\n        }\n        # Add time component - either startTs/endTs OR interval(mutually exclusive)\n        startTs = self.safe_integer(params, 'startTs')\n        endTs = self.safe_integer(params, 'endTs')\n        interval = self.safe_string(params, 'interval')\n        if startTs is not None or endTs is not None:\n            # Use startTs/endTs when provided\n            if startTs is not None:\n                request['startTs'] = startTs\n            if endTs is not None:\n                request['endTs'] = endTs\n        elif interval is not None:\n            # Use interval when startTs/endTs are not provided\n            request['interval'] = interval\n        # Add optional fidelity parameter\n        fidelity = self.safe_number(params, 'fidelity')\n        if fidelity is not None:\n            finalFidelity = fidelity\n            # Polymarket enforces minimum fidelity per interval(e.g. interval=1m requires fidelity>=10)\n            intervalForFidelity = self.safe_string(request, 'interval')\n            if intervalForFidelity == '1m':\n                finalFidelity = max(10, finalFidelity)\n            request['fidelity'] = finalFidelity\n        remainingParams = self.extend({'api_type': 'clob'}, self.extend(request, self.omit(params, ['market', 'startTs', 'endTs', 'fidelity', 'interval'])))\n        return self.request('prices-history', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_time(self, params={}):\n        \"\"\"\n        fetches the current server timestamp from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L178\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # Based on get_server_time() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(TIME = \"/time\")\n        return self.request('time', ['clob', 'public'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_public_get_ok(self, params={}):\n        \"\"\"\n        health check endpoint to confirm server is up\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # Based on get_ok() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py#L170\n        return self.request('', ['clob', 'public'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_public_get_fee_rate(self, params={}):\n        \"\"\"\n        fetches the fee rate for a token from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetFeeRate() requires a token_id parameter')\n        # Based on get_fee_rate() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_FEE_RATE = \"/fee-rate\")\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('fee-rate', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_price(self, params={}):\n        \"\"\"\n        fetches the market price for a specific token and side from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-market-price\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :param str [params.side]: the side: 'BUY' or 'SELL'(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetPrice() requires a token_id parameter')\n        side = self.safe_string(params, 'side')\n        if side is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetPrice() requires a side parameter(BUY or SELL)')\n        # Note: REST API endpoint format: /price?token_id={token_id}&side={side}\n        # See https://docs.polymarket.com/api-reference/pricing/get-market-price\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('price', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_prices(self, params={}):\n        \"\"\"\n        fetches market prices for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch prices for\n        :param str [params.side]: the side: 'BUY' or 'SELL'(required if token_ids provided)\n        :returns dict: response from the exchange\n        \"\"\"\n        # Note: REST API endpoint format: /prices?token_id={token_id1,token_id2,...}\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices\n        # Response format: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        # The endpoint returns both BUY and SELL prices for each token_id\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('prices', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_post_prices(self, params={}):\n        \"\"\"\n        fetches market prices for specified tokens and sides via POST request\n\n        https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param Array [params.requests]: array of {token_id, side} objects(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        requests = self.safe_value(params, 'requests')\n        if requests is None:\n            raise ArgumentsRequired(self.id + ' clobPublicPostPrices() requires a requests parameter(array of {token_id, side} objects)')\n        # Note: REST API endpoint format: POST /prices with JSON body\n        # See https://docs.polymarket.com/api-reference/pricing/get-multiple-market-prices-by-request\n        # Body format: [{\"token_id\": \"1234567890\", \"side\": \"BUY\"}, {\"token_id\": \"1234567890\", \"side\": \"SELL\"}]\n        # Response format: {[token_id]: {BUY: \"price\", SELL: \"price\"}, ...}\n        body = self.json(requests)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'requests'))\n        return self.request('prices', ['clob', 'public'], 'POST', remainingParams, None, body)\n\n    def clob_public_get_midpoint(self, params={}):\n        \"\"\"\n        fetches the midpoint price for a specific token from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-midpoint-price\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetMidpoint() requires a token_id parameter')\n        # Note: REST API endpoint format: /midpoint?token_id={token_id}\n        # See https://docs.polymarket.com/api-reference/pricing/get-midpoint-price\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('midpoint', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_midpoints(self, params={}):\n        \"\"\"\n        fetches midpoint prices for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/pricing/get-midpoint-prices\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch midpoints for(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenIds = self.safe_value(params, 'token_ids')\n        if tokenIds is None or not isinstance(tokenIds, list) or len(tokenIds) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicGetMidpoints() requires a token_ids parameter(array of token IDs)')\n        # Note: REST API endpoint format: POST /midpoints with JSON body\n        # See https://docs.polymarket.com/api-reference/pricing/get-midpoint-prices\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: {[token_id]: \"midpoint\", ...}\n        body: List[Any] = []\n        for i in range(0, len(tokenIds)):\n            body.append({'token_id': tokenIds[i]})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'token_ids'))\n        return self.request('midpoints', ['clob', 'public'], 'POST', remainingParams, None, self.json(body))\n\n    def clob_public_get_spread(self, params={}):\n        \"\"\"\n        fetches the bid-ask spread for a specific token from CLOB API\n\n        https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spread\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetSpread() requires a token_id parameter')\n        # Note: REST API endpoint format: /spread?token_id={token_id}\n        # See https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spread\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('spread', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_last_trade_price(self, params={}):\n        \"\"\"\n        fetches the last trade price for a specific token from CLOB API\n\n        https://docs.polymarket.com/api-reference/trades/get-last-trade-price\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetLastTradePrice() requires a token_id parameter')\n        # Note: REST API endpoint format: /last-trade-price?token_id={token_id}\n        # See https://docs.polymarket.com/api-reference/trades/get-last-trade-price\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('last-trade-price', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_last_trades_prices(self, params={}):\n        \"\"\"\n        fetches last trade prices for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/trades/get-last-trades-prices\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch last trade prices for(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenIds = self.safe_value(params, 'token_ids')\n        if tokenIds is None or not isinstance(tokenIds, list) or len(tokenIds) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicGetLastTradesPrices() requires a token_ids parameter(array of token IDs)')\n        # Note: REST API endpoint format: POST /last-trades-prices with JSON body\n        # See https://docs.polymarket.com/api-reference/trades/get-last-trades-prices\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: {[token_id]: \"price\", ...}\n        body: List[Any] = []\n        for i in range(0, len(tokenIds)):\n            body.append({'token_id': tokenIds[i]})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'token_ids'))\n        return self.request('last-trades-prices', ['clob', 'public'], 'POST', remainingParams, None, self.json(body))\n\n    def clob_public_get_trades(self, params={}):\n        \"\"\"\n        fetches trades for a specific market from CLOB API\n\n        https://docs.polymarket.com/developers/CLOB/clients/methods-public#trades\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: the token ID or condition ID(required)\n        :param int [params.limit]: maximum number of trades to return(default: 100, max: 500)\n        :param str [params.side]: filter by side: 'BUY' or 'SELL'\n        :param int [params.start_timestamp]: start timestamp in seconds\n        :param int [params.end_timestamp]: end timestamp in seconds\n        :returns dict: response from the exchange\n        \"\"\"\n        market = self.safe_string(params, 'market')\n        if market is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetTrades() requires a market(token_id or condition_id) parameter')\n        # Note: REST API endpoint format: /trades?market={token_id}\n        # See https://docs.polymarket.com/developers/CLOB/clients/methods-public#trades\n        request: dict = {\n            'market': market,\n        }\n        limit = self.safe_integer(params, 'limit')\n        if limit is not None:\n            request['limit'] = min(limit, 500)  # Cap at 500\n        side = self.safe_string(params, 'side')\n        if side is not None:\n            request['side'] = side\n        startTimestamp = self.safe_integer(params, 'start_timestamp')\n        if startTimestamp is not None:\n            request['start_timestamp'] = startTimestamp\n        endTimestamp = self.safe_integer(params, 'end_timestamp')\n        if endTimestamp is not None:\n            request['end_timestamp'] = endTimestamp\n        remainingParams = self.extend({'api_type': 'clob'}, self.extend(request, self.omit(params, ['market', 'limit', 'side', 'start_timestamp', 'end_timestamp'])))\n        return self.request('trades', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_tick_size(self, params={}):\n        \"\"\"\n        fetches the tick size for a token from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetTickSize() requires a token_id parameter')\n        # Based on get_tick_size() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_TICK_SIZE = \"/tick-size\")\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('tick-size', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_get_neg_risk(self, params={}):\n        \"\"\"\n        fetches the negative risk flag for a token from CLOB API\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: the token ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenId = self.safe_string(params, 'token_id')\n        if tokenId is None:\n            raise ArgumentsRequired(self.id + ' clobPublicGetNegRisk() requires a token_id parameter')\n        # Based on get_neg_risk() from py-clob-client\n        # See https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_NEG_RISK = \"/neg-risk\")\n        remainingParams = self.extend({'api_type': 'clob'}, params)\n        return self.request('neg-risk', ['clob', 'public'], 'GET', remainingParams)\n\n    def clob_public_post_spreads(self, params={}):\n        \"\"\"\n        fetches bid-ask spreads for multiple tokens from CLOB API\n\n        https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spreads\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.token_ids]: array of token IDs to fetch spreads for(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        tokenIds = self.safe_value(params, 'token_ids')\n        if tokenIds is None or not isinstance(tokenIds, list) or len(tokenIds) == 0:\n            raise ArgumentsRequired(self.id + ' clobPublicPostSpreads() requires a token_ids parameter(array of token IDs)')\n        # Note: REST API endpoint format: POST /spreads\n        # See https://docs.polymarket.com/api-reference/spreads/get-bid-ask-spreads\n        # Request body: [{token_id: \"...\"}, {token_id: \"...\"}, ...]\n        # Response format: {[token_id]: \"spread\", ...}\n        body: List[Any] = []\n        for i in range(0, len(tokenIds)):\n            body.append({'token_id': tokenIds[i]})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'token_ids'))\n        return self.request('spreads', ['clob', 'public'], 'POST', remainingParams, None, self.json(body))\n\n    def clob_private_get_order(self, params={}):\n        \"\"\"\n        fetches a specific order by order ID\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_ORDER = \"/data/order/\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderId = self.safe_string(params, 'order_id')\n        if orderId is None:\n            raise ArgumentsRequired(self.id + ' clobPrivateGetOrder() requires an order_id parameter')\n        path = 'data/order/' + self.encode_uri_component(orderId)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'order_id'))\n        return self.request(path, ['clob', 'private'], 'GET', remainingParams)\n\n    def clob_private_get_orders(self, params={}):\n        \"\"\"\n        fetches orders for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(ORDERS = \"/data/orders\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: filter orders by token ID\n        :param str [params.status]: filter orders by status(OPEN, FILLED, CANCELLED, etc.)\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('data/orders', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_post_order(self, params={}):\n        \"\"\"\n        creates a new order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(POST_ORDER = \"/order\")\n        https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param dict [params.order]: order object(required)\n        :param str [params.owner]: api key of order owner(required)\n        :param str [params.orderType]: order type: \"FOK\", \"GTC\", \"GTD\"(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        # Build request payload according to API specification\n        # See https://docs.polymarket.com/developers/CLOB/orders/create-order#request-payload-parameters\n        order = self.safe_value(params, 'order')\n        if order is None:\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrder() requires an order parameter')\n        owner = self.safe_string(params, 'owner')\n        if owner is None:\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrder() requires an owner parameter(API key)')\n        orderType = self.safe_string(params, 'orderType')\n        if orderType is None:\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrder() requires an orderType parameter')\n        # Build the complete request payload with top-level fields\n        requestPayload: dict = {\n            'order': order,\n            'owner': owner,\n            'orderType': orderType,\n        }\n        # Add optional parameters if provided\n        clientOrderId = self.safe_string(params, 'clientOrderId')\n        if clientOrderId is not None:\n            requestPayload['clientOrderId'] = clientOrderId\n        postOnly = self.safe_bool(params, 'postOnly')\n        if postOnly is not None:\n            requestPayload['postOnly'] = postOnly\n        # Send the complete request payload body\n        body = self.json(requestPayload)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, ['order', 'owner', 'orderType', 'clientOrderId', 'postOnly']))\n        return self.request('order', ['clob', 'private'], 'POST', remainingParams, None, body)\n\n    def clob_private_post_orders(self, params={}):\n        \"\"\"\n        creates multiple orders in a batch\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(POST_ORDERS = \"/orders\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param Array [params.orders]: array of order objects(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orders = self.safe_value(params, 'orders')\n        if orders is None or not isinstance(orders, list):\n            raise ArgumentsRequired(self.id + ' clobPrivatePostOrders() requires an orders parameter(array of order objects)')\n        body = self.json(orders)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'orders'))\n        return self.request('orders', ['clob', 'private'], 'POST', remainingParams, None, body)\n\n    def clob_private_delete_order(self, params={}):\n        \"\"\"\n        cancels an order\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL = \"/order\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID to cancel(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderId = self.safe_string(params, 'order_id')\n        if orderId is None:\n            raise ArgumentsRequired(self.id + ' clobPrivateDeleteOrder() requires an order_id parameter')\n        request: dict = {\n            'orderID': orderId,\n        }\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'order_id'))\n        body = self.json(request)\n        return self.request('order', ['clob', 'private'], 'DELETE', remainingParams, None, body)\n\n    def clob_private_delete_orders(self, params={}):\n        \"\"\"\n        cancels multiple orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ORDERS = \"/orders\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.order_ids]: array of order IDs to cancel(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderIds = self.safe_value(params, 'order_ids')\n        if orderIds is None or not isinstance(orderIds, list):\n            raise ArgumentsRequired(self.id + ' clobPrivateDeleteOrders() requires an order_ids parameter(array of order IDs)')\n        body = self.json(orderIds)\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, 'order_ids'))\n        return self.request('orders', ['clob', 'private'], 'DELETE', remainingParams, None, body)\n\n    def clob_private_delete_cancel_all(self, params={}):\n        \"\"\"\n        cancels all open orders\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(CANCEL_ALL = \"/cancel-all\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: optional token ID to cancel all orders for a specific market\n        :returns dict: response from the exchange\n        \"\"\"\n        body = self.json(params)\n        return self.request('cancel-all', ['clob', 'private'], 'DELETE', {'api_type': 'clob'}, None, body)\n\n    def clob_private_delete_cancel_market_orders(self, params={}):\n        \"\"\"\n        cancels all orders from a market\n\n        https://docs.polymarket.com/developers/CLOB/orders/cancel-market-orders\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.market]: condition id of the market\n        :param str [params.asset_id]: id of the asset/token\n        :returns dict: response from the exchange\n        \"\"\"\n        request: dict = {}\n        market = self.safe_string(params, 'market')\n        if market is not None:\n            request['market'] = market\n        assetId = self.safe_string(params, 'asset_id')\n        if assetId is not None:\n            request['asset_id'] = assetId\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, ['market', 'asset_id']))\n        body = self.json(request)\n        return self.request('cancel-market-orders', ['clob', 'private'], 'DELETE', remainingParams, None, body)\n\n    def clob_private_get_trades(self, params={}):\n        \"\"\"\n        fetches trade history for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py(get_trades method)\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: filter trades by token ID\n        :param int [params.start_timestamp]: start timestamp in seconds\n        :param str [params.next_cursor]: pagination cursor\n        :returns dict: response from the exchange\n        \"\"\"\n        # NOTE: the authenticated L2 endpoint is `/trades`(without the public `/data/` prefix).\n        # Using the public path would return all market trades instead of the caller's own fills.\n        return self.request('trades', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_get_builder_trades(self, params={}):\n        \"\"\"\n        fetches trades originated by the builder\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_BUILDER_TRADES = \"/builder-trades\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.token_id]: filter trades by token ID\n        :param int [params.start_timestamp]: start timestamp in seconds\n        :param str [params.next_cursor]: pagination cursor\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('builder-trades', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_get_notifications(self, params={}):\n        \"\"\"\n        fetches notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_NOTIFICATIONS = \"/notifications\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('notifications', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_delete_notifications(self, params={}):\n        \"\"\"\n        drops notifications for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(DROP_NOTIFICATIONS = \"/notifications\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.notification_id]: specific notification ID to drop\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('notifications', ['clob', 'private'], 'DELETE', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_get_balance_allowance(self, params={}):\n        \"\"\"\n        fetches balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(GET_BALANCE_ALLOWANCE = \"/balance-allowance\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        return self.request('balance-allowance', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_put_balance_allowance(self, params={}):\n        \"\"\"\n        updates balance and allowance for the authenticated user\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(UPDATE_BALANCE_ALLOWANCE = \"/balance-allowance\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param int [params.signature_type]: signature type(default: from options.signatureType or options.signatureTypes.EOA).\n        :returns dict: response from the exchange\n        \"\"\"\n        body = self.json(params)\n        return self.request('balance-allowance', ['clob', 'private'], 'PUT', {'api_type': 'clob'}, None, body)\n\n    def clob_private_get_is_order_scoring(self, params={}):\n        \"\"\"\n        checks if an order is currently scoring\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(IS_ORDER_SCORING = \"/is-order-scoring\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str [params.order_id]: the order ID(required)\n        :param str [params.token_id]: the token ID(required)\n        :param str [params.side]: the side: 'BUY' or 'SELL'(required)\n        :param str [params.price]: the price(required)\n        :param str [params.size]: the size(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        # GET /order-scoring?order_id=...\n        return self.request('order-scoring', ['clob', 'private'], 'GET', self.extend({'api_type': 'clob'}, params))\n\n    def clob_private_post_are_orders_scoring(self, params={}):\n        \"\"\"\n        checks if multiple orders are currently scoring\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/endpoints.py(ARE_ORDERS_SCORING = \"/are-orders-scoring\")\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param str[] [params.orderIds]: array of order IDs to check(required)\n        :returns dict: response from the exchange\n        \"\"\"\n        orderIds = self.safe_value_2(params, 'orderIds', 'order_ids')\n        if orderIds is None or not isinstance(orderIds, list):\n            raise ArgumentsRequired(self.id + ' clobPrivatePostAreOrdersScoring() requires an orderIds parameter(array of order IDs)')\n        body = self.json({'orderIds': orderIds})\n        remainingParams = self.extend({'api_type': 'clob'}, self.omit(params, ['orderIds', 'order_ids']))\n        # POST /orders-scoring with JSON body {orderIds: [...]}\n        return self.request('orders-scoring', ['clob', 'private'], 'POST', remainingParams, None, body)\n\n    def get_main_wallet_address(self):\n        \"\"\"\n        gets main wallet address(walletAddress or options.funder)\n        :returns str: main wallet address\n        \"\"\"\n        if self.walletAddress is not None and self.walletAddress != '':\n            return self.walletAddress\n        funder = self.safe_string(self.options, 'funder')\n        if funder is not None and funder != '':\n            return funder\n        raise ArgumentsRequired(self.id + ' getMainWalletAddress() requires a wallet address. Set `walletAddress` or `options.funder`.')\n\n    def get_proxy_wallet_address(self):\n        \"\"\"\n        gets proxy wallet address for Data-API endpoints(falls back to main wallet if not set)\n        :returns str: proxy wallet address\n        \"\"\"\n        if self.uid is not None and self.uid != '':\n            return self.uid\n        proxyWallet = self.safe_string(self.options, 'proxyWallet')\n        if proxyWallet is not None and proxyWallet != '':\n            return proxyWallet\n        # Fall back to main wallet if proxyWallet is not set\n        return self.get_main_wallet_address()\n\n    def get_builder_wallet_address(self):\n        \"\"\"\n        gets builder wallet address(falls back to main wallet if not set)\n        :returns str: builder wallet address\n        \"\"\"\n        builderWallet = self.safe_string(self.options, 'builderWallet')\n        if builderWallet is not None and builderWallet != '':\n            return builderWallet\n        # Fall back to main wallet if builderWallet is not set\n        return self.get_main_wallet_address()\n\n    def get_user_total_value(self, userAddress: str = None) -> dict:\n        \"\"\"\n        fetches total value of a user's positions from Data-API\n\n        https://docs.polymarket.com/api-reference/core/get-total-value-of-a-users-positions\n\n        :param str [userAddress]: user wallet address(defaults to getProxyWalletAddress())\n        :returns dict: object with 'value'(number) and 'response'(raw API response)\n        \"\"\"\n        address: str = None\n        if userAddress is not None:\n            # Use provided address directly(public endpoint, no wallet setup needed)\n            address = userAddress\n        else:\n            # Try to get proxy wallet address, but handle case where wallet is not configured\n            # This allows public calls without requiring wallet setup\n            try:\n                address = self.get_proxy_wallet_address()\n            except Exception as e:\n                # If wallet is not configured, require userAddress parameter for public calls\n                raise ArgumentsRequired(self.id + ' getUserTotalValue() requires a userAddress parameter when wallet is not configured. This is a public endpoint that can query any user address.')\n        # Fetch total value from Data-API\n        valueResponse = self.data_public_get_total_value({'user': address})\n        # Response format: [{\"user\": \"0x...\", \"value\": 123}]\n        valueData = valueResponse\n        if isinstance(valueResponse, list):\n            if len(valueResponse) > 0:\n                valueData = valueResponse[0]\n            else:\n                valueData = {}\n        totalValue = self.safe_number(valueData, 'value', 0)\n        return {\n            'value': totalValue,\n            'response': valueResponse,\n        }\n\n    def get_user_positions(self, userAddress: str = None, params={}) -> dict:\n        \"\"\"\n        fetches current positions for a user from Data-API(defaults to proxy wallet)\n\n        https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user\n\n        :param str [userAddress]: user wallet address(defaults to getProxyWalletAddress())\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        # TODO add pagination, sort, limit etc https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user\n        address: str = None\n        if userAddress is not None:\n            # Use provided address directly(public endpoint, no wallet setup needed)\n            address = userAddress\n        else:\n            # Try to get proxy wallet address, but handle case where wallet is not configured\n            # This allows public calls without requiring wallet setup\n            try:\n                address = self.get_proxy_wallet_address()\n            except Exception as e:\n                # If wallet is not configured, require userAddress parameter for public calls\n                raise ArgumentsRequired(self.id + ' getUserPositions() requires a userAddress parameter when wallet is not configured. This is a public endpoint that can query any user address.')\n        return self.data_public_get_positions(self.extend({'user': address}, params))\n\n    def get_user_activity(self, userAddress: str = None, params={}) -> dict:\n        \"\"\"\n        fetches user activity from Data-API(defaults to proxy wallet)\n\n        https://docs.polymarket.com/api-reference/core/get-user-activity\n\n        :param str [userAddress]: user wallet address(defaults to getProxyWalletAddress())\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict: response from the exchange\n        \"\"\"\n        address: str = None\n        if userAddress is not None:\n            # Use provided address directly(public endpoint, no wallet setup needed)\n            address = userAddress\n        else:\n            # Try to get proxy wallet address, but handle case where wallet is not configured\n            # This allows public calls without requiring wallet setup\n            try:\n                address = self.get_proxy_wallet_address()\n            except Exception as e:\n                # If wallet is not configured, require userAddress parameter for public calls\n                raise ArgumentsRequired(self.id + ' getUserActivity() requires a userAddress parameter when wallet is not configured. This is a public endpoint that can query any user address.')\n        request: dict = {\n            'user': address,\n            'limit': self.safe_integer(params, 'limit', 100),\n            'offset': self.safe_integer(params, 'offset', 0),\n            'sortBy': self.safe_string(params, 'sortBy', 'TIMESTAMP'),\n            'sortDirection': self.safe_string(params, 'sortDirection', 'DESC'),\n        }\n        return self.data_public_get_activity(self.extend(request, self.omit(params, ['user'])))\n\n    def parse_user_activity(self, activity: dict, market: Market = None) -> dict:\n        \"\"\"\n        parse a raw user activity record into a trade-like structure consumable by parseTrades\n        :param dict activity: raw activity payload from Data-API\n        :param dict [market]: market structure, when known\n        :returns dict|None: normalized activity(only for TRADE records) or None\n        \"\"\"\n        activityType = self.safe_string(activity, 'type')\n        if activityType != 'TRADE':\n            return None\n        rawTs = self.safe_integer(activity, 'timestamp')\n        isoTimestamp = self.safe_string(activity, 'timestamp')\n        if rawTs is not None:\n            tsMs = rawTs * 1000 if (rawTs < 1000000000000) else rawTs\n            isoTimestamp = self.iso8601(tsMs)\n        symbol = market['symbol'] if (market is not None) else self.safe_string(activity, 'condition_id')\n        return self.extend(activity, {\n            'timestamp': isoTimestamp,\n            'transactionHash': self.safe_string(activity, 'transactionHash'),\n            'symbol': symbol,\n            'asset': self.safe_string(activity, 'asset'),\n            'price': self.safe_number(activity, 'price'),\n            'size': self.safe_number(activity, 'size'),\n            'side': self.safe_string(activity, 'side'),\n        })\n\n    def format_address(self, address: str = None):\n        if address is None:\n            return None\n        if address.startswith('0x'):\n            return address.replace('0x', '')\n        return address\n\n    def normalize_address(self, address: str) -> str:\n        normalized = str(address).strip()\n        if not normalized.startswith('0x'):\n            normalized = '0x' + normalized\n        return normalized.lower()\n\n    def hash_message(self, message: str) -> str:\n        binaryMessage = self.encode(message)\n        binaryMessageLength = self.binary_length(binaryMessage)\n        x19 = self.base16_to_binary('19')\n        newline = self.base16_to_binary('0a')\n        prefix = self.binary_concat(x19, self.encode('Ethereum Signed Message:'), newline, self.encode(self.number_to_string(binaryMessageLength)))\n        return '0x' + self.hash(self.binary_concat(prefix, binaryMessage), 'keccak', 'hex')\n\n    def get_contract_config(self, chainID: float) -> dict:\n        contracts = self.safe_value(self.options, 'contracts', {})\n        chainIdStr = str(chainID)\n        contractConfig = self.safe_value(contracts, chainIdStr)\n        if contractConfig is None:\n            raise ExchangeError(self.id + ' getContractConfig() invalid network chainId: ' + chainIdStr)\n        return contractConfig\n\n    def sign_message(self, message: str, privateKey: str) -> str:\n        hash = self.hash_message(message)\n        return self.sign_hash(hash, privateKey)\n\n    def sign_hash(self, hash: str, privateKey: str):\n        signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None)\n        r = signature['r']\n        s = signature['s']\n        v = self.int_to_base16(self.sum(27, signature['v']))\n        # Convert to lowercase hex(Ethereum standard)\n        finalSignature = ('0x' + r.rjust(64, '0') + s.rjust(64, '0') + v.rjust(2, '0')).lower()\n        return finalSignature\n\n    def sign_typed_data(self, domain: dict, types: dict, value: dict) -> str:\n        # This returns binary data: 0x1901 or hashDomain(domain) or hashStruct(primaryType, types, value)\n        encoded = self.eth_encode_structured_data(domain, types, value)\n        # Hash the encoded binary data with keccak256\n        hash = '0x' + self.hash(encoded, 'keccak', 'hex')\n        # Sign the hash using signHash\n        signature = self.sign_hash(hash, self.privateKey)\n        return signature\n\n    def create_level1_headers(self, walletAddress: str, nonce: float = None) -> dict:\n        if walletAddress is None or walletAddress == '':\n            raise ArgumentsRequired(self.id + ' createLevel1Headers() requires a valid walletAddress')\n        normalizedAddress = self.normalize_address(walletAddress)\n        chainId = self.safe_integer(self.options, 'chainId')\n        timestampSeconds = int(math.floor(self.milliseconds()) / 1000)\n        timestamp = str(timestampSeconds)\n        nonceValue = 0\n        if nonce is not None:\n            nonceValue = nonce\n        clobDomainName = self.safe_string(self.options, 'clobDomainName')\n        clobVersion = self.safe_string(self.options, 'clobVersion')\n        msgToSign = self.safe_string(self.options, 'msgToSign')\n        domain = {\n            'name': clobDomainName,\n            'version': clobVersion,\n            'chainId': chainId,\n        }\n        # https://github.com/Polymarket/clob-client/blob/b75aec68be17190215b7230372fbedfe85de20ef/src/signing/eip712.ts#L28\n        types = {\n            'ClobAuth': [\n                {'name': 'address', 'type': 'address'},\n                {'name': 'timestamp', 'type': 'string'},\n                {'name': 'nonce', 'type': 'uint256'},\n                {'name': 'message', 'type': 'string'},\n            ],\n        }\n        message = {\n            'address': normalizedAddress,\n            'timestamp': timestamp,\n            'nonce': nonceValue,\n            'message': msgToSign,\n        }\n        signature = self.sign_typed_data(domain, types, message)\n        headers = {\n            'POLY_ADDRESS': normalizedAddress,\n            'POLY_TIMESTAMP': timestamp,\n            'POLY_NONCE': str(nonceValue),\n            'POLY_SIGNATURE': signature,\n        }\n        return headers\n\n    def get_clob_base_url(self, params={}) -> str:\n        \"\"\"\n        Gets the CLOB API base URL(handles sandbox mode and custom hosts)\n        :param dict [params]: extra parameters\n        :returns str: base URL for CLOB API\n        \"\"\"\n        apiType = self.safe_string(params, 'api_type', 'clob')\n        baseUrl = self.urls['api'][apiType]\n        # Check for sandbox mode\n        if self.isSandboxModeEnabled and self.urls['test'] and self.urls['test'][apiType]:\n            baseUrl = self.urls['test'][apiType]\n        if apiType == 'clob':\n            customHost = self.safe_string(self.options, 'clobHost')\n            if customHost is not None:\n                baseUrl = customHost\n        return baseUrl\n\n    def parse_api_credentials(self, response: Any) -> dict:\n        \"\"\"\n        Parses API credentials from API response and caches them\n        :param dict response: API response\n        :returns dict} API credentials {apiKey, secret, passphrase:\n        \"\"\"\n        apiKey = self.safe_string(response, 'apiKey') or self.safe_string(response, 'api_key')\n        secret = self.safe_string(response, 'secret')\n        passphrase = self.safe_string(response, 'passphrase')\n        if not apiKey or not secret or not passphrase:\n            raise ExchangeError(self.id + ' parseApiCredentials() failed to parse credentials. Response: ' + self.json(response))\n        credentials = {\n            'apiKey': apiKey,\n            'secret': secret,\n            'passphrase': passphrase,\n        }\n        # Cache credentials in options\n        self.options['apiCredentials'] = credentials\n        # Also set them properties for use in sign() method\n        self.apiKey = apiKey\n        self.secret = secret\n        self.password = passphrase\n        return credentials\n\n    def create_api_key(self, params={}) -> dict:\n        \"\"\"\n        Creates a new CLOB API key for the given address\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param number [params.nonce]: optional nonce/timestamp\n        :returns dict} API credentials {apiKey, secret, passphrase:\n @note Uses manual URL building instead of self.request() because self endpoint requires L1 authentication\n (POLY_ADDRESS, POLY_SIGNATURE headers) rather than the standard L2 authentication used by self.request()\n        \"\"\"\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' create_api_key() requires a privateKey')\n        # Validate privateKey format(should be hex string with 0x prefix, 66 chars total)\n        if not self.privateKey.startswith('0x') or len(self.privateKey) != 66:\n            raise ArgumentsRequired(self.id + ' create_api_key() requires a valid privateKey(0x-prefixed hex string, 66 characters)')\n        walletAddress = self.get_main_wallet_address()\n        # Validate walletAddress format(should be hex string with 0x prefix, 42 chars total)\n        if not walletAddress.startswith('0x') or len(walletAddress) != 42:\n            raise ArgumentsRequired(self.id + ' create_api_key() requires a valid walletAddress(0x-prefixed hex string, 42 characters). Got: ' + walletAddress)\n        baseUrl = self.get_clob_base_url(params)\n        nonce = self.safe_integer(params, 'nonce')\n        headers = self.create_level1_headers(walletAddress, nonce)\n        url = baseUrl + '/auth/api-key'\n        # POST /auth/api-key(creates new API credentials with L1 authentication)\n        response = self.fetch(url, 'POST', headers, None)\n        return self.parse_api_credentials(response)\n\n    def derive_api_key(self, params={}) -> dict:\n        \"\"\"\n        Derives an already existing CLOB API key for the given address and nonce\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param number [params.nonce]: optional nonce/timestamp\n        :returns dict} API credentials {apiKey, secret, passphrase:\n @note Uses manual URL building instead of self.request() because self endpoint requires L1 authentication\n (POLY_ADDRESS, POLY_SIGNATURE headers) rather than the standard L2 authentication used by self.request()\n        \"\"\"\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' derive_api_key() requires a privateKey')\n        walletAddress = self.get_main_wallet_address()\n        baseUrl = self.get_clob_base_url(params)\n        nonce = self.safe_integer(params, 'nonce')\n        headers = self.create_level1_headers(walletAddress, nonce)\n        url = baseUrl + '/auth/derive-api-key'\n        # GET /auth/derive-api-key(derives existing API credentials with L1 authentication)\n        response = self.fetch(url, 'GET', headers, None)\n        return self.parse_api_credentials(response)\n\n    def create_or_derive_api_creds(self, params={}) -> dict:\n        \"\"\"\n        Creates API creds if not already created for nonce, otherwise derives them\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :param number [params.nonce]: optional nonce/timestamp\n        :returns dict} API credentials {apiKey, secret, passphrase:\n        \"\"\"\n        # Check if credentials are already cached\n        cachedCreds = self.safe_dict(self.options, 'apiCredentials')\n        if cachedCreds is not None:\n            return cachedCreds\n        # Try create_api_key first, then derive_api_key if create fails\n        # Based on py-clob-client client.py: create_or_derive_api_creds()\n        try:\n            return self.create_api_key(params)\n        except Exception as e:\n            # If create fails(e.g., key already exists), try to derive it\n            return self.derive_api_key(params)\n\n    def set_api_creds(self, credentials: dict):\n        \"\"\"\n        Sets API credentials(alias for caching credentials)\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/client.py\n\n        :param dict credentials: API credentials {apiKey, secret, passphrase}\n        \"\"\"\n        self.options['apiCredentials'] = credentials\n        self.apiKey = self.safe_string(credentials, 'apiKey')\n        self.secret = self.safe_string(credentials, 'secret')\n        self.password = self.safe_string(credentials, 'passphrase')\n\n    def get_api_base_url(self, params={}) -> str:\n        \"\"\"\n        Gets the API base URL for the specified API type(handles sandbox mode and custom hosts)\n        :param dict [params]: extra parameters\n        :param str [params.api_type]: API type('clob', 'gamma', 'data', etc.)\n        :returns str: base URL for the API\n        \"\"\"\n        apiType = self.safe_string(params, 'api_type', 'clob')\n        # Ensure urls.api exists\n        if self.urls is None or self.urls['api'] is None:\n            raise ExchangeError(self.id + ' getApiBaseUrl() failed: urls.api is not initialized. Make sure exchange is properly initialized.')\n        # Direct access to nested object property\n        baseUrl = self.urls['api'][apiType]\n        # Check for sandbox mode\n        if self.isSandboxModeEnabled and self.urls['test'] and self.urls['test'][apiType]:\n            baseUrl = self.urls['test'][apiType]\n        # Allow custom CLOB host override\n        if apiType == 'clob':\n            customHost = self.safe_string(self.options, 'clobHost')\n            if customHost is not None:\n                baseUrl = customHost\n        # Ensure we have a valid base URL\n        if baseUrl is None:\n            apiUrls = self.urls['api'] or {}\n            availableTypesList = list(apiUrls.keys())\n            availableTypes = ''\n            if len(availableTypesList) > 0:\n                availableTypes = ', '.join(availableTypesList)\n            raise ExchangeError(self.id + ' getApiBaseUrl() failed: API type \"' + apiType + '\" not found in urls.api. Available types: ' + availableTypes)\n        return baseUrl\n\n    def build_default_headers(self, method: str, existingHeaders: dict = None) -> dict:\n        \"\"\"\n        Builds default HTTP headers based on py-clob-client helpers.py\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/http_helpers/helpers.py\n\n        :param str method: HTTP method('GET', 'POST', etc.)\n        :param dict [existingHeaders]: existing headers to self.extend\n        :returns dict: headers dictionary\n        \"\"\"\n        if existingHeaders is None:\n            existingHeaders = {}\n        headers = self.extend({\n            'User-Agent': 'ccxt',\n            'Accept': '*/*',\n            'Connection': 'keep-alive',\n            'Content-Type': 'application/json',\n        }, existingHeaders)\n        # Add Accept-Encoding for GET requests(as per py-clob-client)\n        if method == 'GET':\n            headers['Accept-Encoding'] = 'gzip'\n        return headers\n\n    def build_public_request(self, baseUrl: str, pathWithParams: str, method: str, queryParams: dict, body: str = None, headers: dict = None) -> dict:\n        \"\"\"\n        Builds a public(unauthenticated) request\n        :param str baseUrl: API base URL\n        :param str pathWithParams: path with parameters\n        :param str method: HTTP method\n        :param dict queryParams: query parameters\n        :param str [body]: request body\n        :param dict [headers]: request headers\n        :returns dict: request object with url, method, body, and headers\n        \"\"\"\n        headers = self.build_default_headers(method, headers)\n        url = baseUrl + '/' + pathWithParams\n        if method == 'GET':\n            if queryParams:\n                url += '?' + self.urlencode(queryParams)\n        else:\n            # For POST requests, body should already be set by the calling method\n            if body is None and queryParams:\n                body = self.json(queryParams)\n        return {'url': url, 'method': method, 'body': body, 'headers': headers}\n\n    def ensure_api_credentials(self, params={}) -> dict:\n        \"\"\"\n        Ensures API credentials are generated(lazy generation, similar to dYdX's retrieveCredentials)\n        :param dict [params]: extra parameters specific to the exchange API endpoint\n        :returns dict} API credentials {apiKey, secret, passphrase:\n        \"\"\"\n        # Check if credentials are already cached\n        cachedCreds = self.safe_dict(self.options, 'apiCredentials')\n        if cachedCreds is not None:\n            return cachedCreds\n        # Check if credentials are provided directly(apiKey, secret, password)\n        # This allows users to provide credentials directly instead of generating from privateKey\n        if self.apiKey and self.secret and self.password:\n            directCreds = {\n                'apiKey': self.apiKey,\n                'secret': self.secret,\n                'passphrase': self.password,\n            }\n            self.set_api_creds(directCreds)\n            return directCreds\n        # If direct credentials not provided, check if privateKey is available for generation\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' ensureApiCredentials() requires either: (1) apiKey + secret + password provided directly, or (2) privateKey to generate credentials')\n        # Generate credentials lazily(similar to dYdX's retrieveCredentials pattern)\n        # This is called automatically before authenticated requests\n        creds = self.create_or_derive_api_creds(params)\n        self.set_api_creds(creds)\n        return creds\n\n    def get_api_credentials(self) -> dict:\n        \"\"\"\n        Gets API credentials from cache or instance properties\n        :returns dict} API credentials {apiKey, secret, password:\n        \"\"\"\n        apiKey = self.apiKey\n        secret = self.secret\n        password = self.password\n        # Check if credentials are already cached\n        cachedCreds = self.safe_dict(self.options, 'apiCredentials')\n        if cachedCreds is not None:\n            apiKey = self.safe_string(cachedCreds, 'apiKey') or apiKey\n            secret = self.safe_string(cachedCreds, 'secret') or secret\n            password = self.safe_string(cachedCreds, 'passphrase') or password\n        # If credentials are not available, check if privateKey is set\n        # Only raise error if privateKey is set(meaning user wants authenticated requests)\n        # This allows public requests to work even when privateKey is set but credentials not yet generated\n        if not apiKey or not secret or not password:\n            if self.privateKey is None:\n                # No privateKey set - self should not happen if called from buildPrivateRequest\n                raise ArgumentsRequired(self.id + ' getApiCredentials() called but no credentials available and no privateKey set. This should only be called for authenticated requests. Provide either: (1) apiKey + secret + password directly, or (2) privateKey to generate credentials.')\n            # privateKey is set but credentials not generated yet - self is expected for lazy generation\n            # Don't raise error here, ensureApiCredentials() handle it\n            raise ArgumentsRequired(self.id + ' API credentials not generated. Credentials are automatically generated on first authenticated request, but privateKey is required. Alternatively, provide apiKey + secret + password directly.')\n        return {'apiKey': apiKey, 'secret': secret, 'password': password}\n\n    def build_request_path_and_payload(self, pathWithParams: str, method: str, queryParams: dict, body: str = None) -> dict:\n        \"\"\"\n        Builds the request path and payload for signature\n        :param str pathWithParams: path with parameters\n        :param str method: HTTP method\n        :param dict queryParams: query parameters\n        :param str [body]: request body\n        :returns dict} {requestPath, url, payload, body:\n        \"\"\"\n        # Ensure path doesn't have double slashes(pathWithParams may already start with /)\n        normalizedPath = pathWithParams if pathWithParams.startswith('/') else '/' + pathWithParams\n        requestPath = normalizedPath\n        url = requestPath\n        payload = ''\n        if method == 'GET':\n            if queryParams:\n                queryString = self.urlencode(queryParams)\n                url += '?' + queryString\n                payload = queryString\n        else:\n            # For POST/PUT/DELETE, body is part of the signature\n            # Use deterministic JSON serialization(no spaces, compact) matching py-clob-client\n            # json.dumps(body, separators=(\",\", \":\"), ensure_ascii=False) produces compact JSON\n            if body is None and queryParams:\n                # json.dumpsby default produces compact JSON(no spaces)\n                body = json.dumps(queryParams)\n            # Serialize body deterministically if it's an object\n            if body is not None and isinstance(body, dict):\n                body = json.dumps(body)\n            # Use body(quote replacement happens in createLevel2Signature)\n            payload = str(body) if (body is not None and body != '') else ''\n        return {'requestPath': requestPath, 'url': url, 'payload': payload, 'body': body}\n\n    def create_level2_signature(self, timestamp: str, method: str, requestPath: str, body: str, secret: str) -> str:\n        \"\"\"\n        Creates Level 2 authentication signature(HMAC-SHA256)\n\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param str timestamp: timestamp string\n        :param str method: HTTP method\n        :param str requestPath: request path\n        :param str body: request body(serialized JSON string)\n        :param str secret: API secret(base64 encoded, URL-safe)\n        :returns str: URL-safe base64 encoded signature\n        \"\"\"\n        # Create signature: HMAC-SHA256(timestamp + method + path + body, secret)\n        # Based on Polymarket CLOB API L2 authentication(matches py-clob-client build_hmac_signature)\n        # Use str(method) to preserve case(don't use toUpperCase())\n        message = str(timestamp) + str(method) + str(requestPath)\n        # Only add body if it exists and is not empty\n        # NOTE: Replace single quotes with double quotes(matching py-clob-client behavior)\n        # This is necessary to generate the same hmac message and typescript\n        messageWithBody = message\n        if body is not None and body != '':\n            messageWithBody = message + str(body).replace(\"'\", '\"')\n        # Generate HMAC and return URL-safe base64\n        # Convert URL-safe base64 to standard base64(replace - with + and _ with /)\n        secretBinary = self.base64_to_binary(str(secret).replace('-', '+').replace('_', '/'))\n        hmacResult = self.hmac(self.encode(messageWithBody), secretBinary, hashlib.sha256, 'base64')\n        return hmacResult.replace('+', '-').replace('/', '_')\n\n    def create_level2_headers(self, apiKey: str, timestamp: str, signature: str, password: str) -> dict:\n        \"\"\"\n        Creates Level 2 authentication headers\n\n        https://github.com/Polymarket/py-clob-client/blob/main/py_clob_client/headers/headers.py\n\n        :param str apiKey: API key\n        :param str timestamp: timestamp string\n        :param str signature: signature string\n        :param str password: API passphrase\n        :returns dict: Level 2 headers dictionary\n        \"\"\"\n        authHeaders: dict = {\n            'POLY_API_KEY': apiKey,\n            'POLY_TIMESTAMP': timestamp,\n            'POLY_SIGNATURE': signature,\n            'POLY_PASSPHRASE': password,  # Passphrase is required for L2 authentication\n            'Content-Type': 'application/json',\n        }\n        # Always include POLY_ADDRESS in Level 2 headers(matches GitHub issue  #190 fix)\n        # Get wallet address from funder option, walletAddress property, or derive from privateKey\n        walletAddress = self.safe_string(self.options, 'funder')\n        if walletAddress is None and self.walletAddress is not None:\n            walletAddress = self.walletAddress\n        if walletAddress is None and self.privateKey is not None:\n            # Derive wallet address from private key if not provided\n            walletAddress = self.get_main_wallet_address()\n        if walletAddress is not None:\n            # Normalize and checksum the address(EIP-55)\n            walletAddress = self.normalize_address(walletAddress)\n            authHeaders['POLY_ADDRESS'] = walletAddress\n        #  # Add signature type if provided(defaults to EOA from options)\n        # signatureType = self.get_signature_type(params)\n        # eoaSignatureType = self.safe_integer(self.safe_dict(self.options, 'signatureTypes', {}), 'EOA', 0)\n        # if signatureType != eoaSignatureType:\n        #     authHeaders['POLY_SIGNATURE_TYPE'] = str(signatureType)\n        # }\n        #  # Add chain ID(defaults to 137 for Polygon mainnet, 80001 for testnet)\n        #  # chain_id: 137 = Polygon mainnet(default), 80001 = Polygon Mumbai testnet\n        # chainId = self.safe_integer(self.options, 'chainId', 137)\n        # authHeaders['POLY_CHAIN_ID'] = str(chainId)\n        return authHeaders\n\n    def build_private_request(self, baseUrl: str, pathWithParams: str, method: str, queryParams: dict, body: str = None, headers: dict = None) -> dict:\n        \"\"\"\n        Builds a private(authenticated) request with L2 authentication\n        :param str baseUrl: API base URL\n        :param str pathWithParams: path with parameters\n        :param str method: HTTP method\n        :param dict queryParams: query parameters\n        :param str [body]: request body\n        :param dict [headers]: existing headers\n        :returns dict: request object with url, method, body, and headers\n        \"\"\"\n        # Ensure privateKey is set\n        if self.privateKey is None:\n            raise ArgumentsRequired(self.id + ' requires privateKey for authenticated requests')\n        # Get API credentials - self will raise if credentials not generated\n        # For lazy generation, ensureApiCredentials() should be called before self\n        creds = self.get_api_credentials()\n        timestamp = str(self.nonce())\n        # Serialize body deterministically if it's an object(matching py-clob-client)\n        # Use json.dumpswhich produces compact JSON by default(no spaces)\n        # This matches: json.dumps(body, separators=(\",\", \":\"), ensure_ascii=False)\n        serializedBody: str = None\n        if body is not None:\n            if isinstance(body, dict):\n                # Deterministic JSON: compact format(no spaces)\n                serializedBody = json.dumps(body)\n            else:\n                serializedBody = str(body)\n        elif queryParams and (method == 'POST' or method == 'PUT' or method == 'DELETE'):\n            # If body is None but we have queryParams for POST/PUT/DELETE, serialize them\n            serializedBody = json.dumps(queryParams)\n        # Build request path and payload using the serialized body\n        pathAndPayload = self.build_request_path_and_payload(pathWithParams, method, queryParams, serializedBody)\n        requestPath = pathAndPayload['requestPath']\n        requestUrl = pathAndPayload['url']\n        # Use the serialized body for the actual request(exact string that will be sent)\n        finalBody = serializedBody is not serializedBody if None else pathAndPayload['body']\n        privateUrl = baseUrl + requestUrl\n        # Create Level 2 signature: for GET requests, do NOT include query params in signature\n        # For POST/PUT/DELETE, include the serialized body(not query params)\n        # This matches py-clob-client: signature = timestamp + method + requestPath [+ body for non-GET]\n        bodyForSignature = None if (method == 'GET') else serializedBody\n        signature = self.create_level2_signature(timestamp, method, requestPath, bodyForSignature, creds['secret'])\n        # Create Level 2 headers\n        authHeaders = self.create_level2_headers(creds['apiKey'], timestamp, signature, creds['password'])\n        # Merge with existing headers\n        headers = self.build_default_headers(method, headers)\n        headers = self.extend(headers, authHeaders)\n        return {'url': privateUrl, 'method': method, 'body': finalBody, 'headers': headers}\n\n    def sign(self, path, api: Any = [ 'clob', 'public' ], method='GET', params={}, headers=None, body=None):\n        \"\"\"\n        Signs a request for authenticated endpoints\n\n        https://docs.polymarket.com/developers/CLOB/authentication\n\n        :param str path: API endpoint path\n        :param str api: API type('public' or 'private')\n        :param str method: HTTP method('GET', 'POST', etc.)\n        :param dict params: Request parameters\n        :param dict headers: Request headers\n        :param str body: Request body\n        :returns dict: Signed request with url, method, body, and headers\n        \"\"\"\n        # Get API base URL\n        baseUrl = self.get_api_base_url(params)\n        # Build path with parameters\n        pathWithParams = self.implode_params(path, params)\n        query = self.omit(params, self.extract_params(path))\n        # Remove api_type from query params's not part of the actual API request\n        queryParams = self.omit(query, ['api_type'])\n        # For public endpoints, no authentication needed\n        # api is always an array like ['gamma', 'public'] or ['clob', 'private']\n        # The second element is the access level(public/private)\n        accessLevel = self.safe_string(api, 1, 'public')\n        if accessLevel == 'public':\n            return self.build_public_request(baseUrl, pathWithParams, method, queryParams, body, headers)\n        # For private endpoints, use L2 authentication\n        return self.build_private_request(baseUrl, pathWithParams, method, queryParams, body, headers)\n\n    def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response: Any, requestHeaders: Any, requestBody: Any):\n        if response is None:\n            return None\n        # Polymarket API errors\n        if code >= 400:\n            # Explicitly check for 401(Unauthorized) and raise AuthenticationError\n            if code == 401:\n                authFeedback = self.id + ' ' + method + ' ' + url + ' 401 ' + reason + ' ' + body\n                raise AuthenticationError(authFeedback)\n            # Try to parse error message from response first(can be JSON or text)\n            # Check error message BEFORE status code to catch specific errors like \"Order not found\"\n            # that may return 400 status but should raise OrderNotFound instead of BadRequest\n            errorMessage = None\n            errorData = None\n            try:\n                if isinstance(response, str):\n                    errorMessage = response\n                elif isinstance(response, dict):\n                    errorMessage = self.safe_string(response, 'error')\n                    if errorMessage is None:\n                        errorMessage = self.safe_string(response, 'message')\n                    if errorMessage is None:\n                        # If no error/message field, use the whole response data\n                        errorData = response\n            except Exception as e:\n                errorMessage = body\n            feedback = self.id + ' ' + (errorMessage or body)\n            if errorMessage is not None:\n                # Try exact match first(e.g., \"Order not found\" -> OrderNotFound)\n                self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback)\n                # Then try broad match\n                self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback)\n                # If no match, fall through to status code check\n            # Check HTTP status code(use throwExactlyMatchedException for proper type handling)\n            # This handles cases where no specific error message is found in the response\n            codeAsString = str(code)\n            statusCodeFeedback = self.id + ' ' + method + ' ' + url + ' ' + codeAsString + ' ' + reason + ' ' + body\n            self.throw_exactly_matched_exception(self.exceptions['exact'], codeAsString, statusCodeFeedback)\n            # If we reach here, no exception was thrown, so raise a generic error\n            if errorData is not None:\n                raise ExchangeError(self.id + ' ' + self.json(errorData))\n            else:\n                raise ExchangeError(feedback)\n        return None\n"
  },
  {
    "path": "Trading/Exchange/polymarket/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"Polymarket\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/polymarket/polymarket_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants\n\n\nclass PolymarketConnector(exchanges.CCXTConnector):\n\n    def _client_factory(\n        self,\n        force_unauth,\n        keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None\n    ) -> tuple:\n        return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)\n\n    def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:\n        # if api key and secret are provided, use them as wallet address and private key\n        creds.wallet_address = creds.api_key\n        creds.uid = creds.password\n        creds.private_key = creds.secret\n        creds.api_key = creds.secret = creds.password = None\n        return creds\n\nclass Polymarket(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    DEFAULT_CONNECTOR_CLASS = PolymarketConnector\n\n    SUPPORT_FETCHING_CANCELLED_ORDERS = False\n\n    @classmethod\n    def get_name(cls):\n        return 'polymarket'\n\n    def get_additional_connector_config(self):\n        return {\n            ccxt_constants.CCXT_OPTIONS: {\n                \"fetchMarkets\": {\n                    \"types\": [\"option\"],  # only polymarket option markets are supported\n                }\n            }\n        }\n"
  },
  {
    "path": "Trading/Exchange/polymarket/resources/Polymarket.md",
    "content": "Polymarket is a complete RestExchange adaptation for Polymarket platform. \n"
  },
  {
    "path": "Trading/Exchange/polymarket/script/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nfrom .ccxt import CCXTPolymarketExchange, CCXTAsyncPolymarketExchange, CCXTProPolymarketExchange\nfrom .polymarket_exchange import Polymarket\n"
  },
  {
    "path": "Trading/Exchange/polymarket/script/download.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport shutil\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple, Any\n\nCCXT_PATH = '../../../../../ccxt'\nFILE_MAPPINGS: Dict[str, Dict[str, Any]] = {\n    f'{CCXT_PATH}/python/ccxt/polymarket.py': {\n        'destination': '../ccxt/polymarket_sync.py',\n        'patches': [\n            ('from ccxt.abstract.polymarket import ImplicitAPI', 'from .polymarket_abstract import ImplicitAPI'),\n        ],\n    },\n    f'{CCXT_PATH}/python/ccxt/async_support/polymarket.py': {\n        'destination': '../ccxt/polymarket_async.py',\n        'patches': [\n            ('from ccxt.abstract.polymarket import ImplicitAPI', 'from .polymarket_abstract import ImplicitAPI'),\n        ],\n    },\n    f'{CCXT_PATH}/python/ccxt/pro/polymarket.py': {\n        'destination': '../ccxt/polymarket_pro.py',\n        'patches': [\n            ('import ccxt.async_support', 'from .polymarket_async import polymarket'),\n            ('class polymarket(ccxt.async_support.polymarket):', 'class polymarket(polymarket):'),\n        ],\n    },\n    f'{CCXT_PATH}/python/ccxt/abstract/polymarket.py': {\n        'destination': '../ccxt/polymarket_abstract.py',\n        'patches': [],\n    },\n}\n\n\ndef apply_patches(file_path: str, patches: List[Tuple[str, str]]) -> None:\n    \"\"\"\n    Apply patches to the copied file.\n    \n    Args:\n        file_path: Path to the destination file\n        patches: List of (old_string, new_string) tuples to replace\n    \"\"\"\n    if not patches:\n        return\n    \n    with open(file_path, 'r', encoding='utf-8') as f:\n        content = f.read()\n    \n    original_content = content\n    \n    # Apply each patch\n    for old_string, new_string in patches:\n        if old_string in content:\n            content = content.replace(old_string, new_string)\n            print(f\"  Applied patch: {old_string[:50]}... -> {new_string[:50]}...\")\n        else:\n            print(f\"  Warning: Patch pattern not found: {old_string[:50]}...\")\n    \n    # Only write if content changed\n    if content != original_content:\n        with open(file_path, 'w', encoding='utf-8') as f:\n            f.write(content)\n        print(f\"  Patches applied successfully\")\n    else:\n        print(f\"  No changes made (patches may have already been applied)\")\n\n\ndef copy_files() -> None:\n    \"\"\"\n    Copy files from ccxt directory to local ccxt directory with optional patches.\n    \"\"\"\n    # Get the directory where this script is located\n    script_dir = Path(__file__).parent.absolute()\n    \n    for source_rel, config in FILE_MAPPINGS.items():\n        dest_rel = config['destination']\n        patches = config.get('patches', [])\n        \n        # Resolve paths relative to script directory\n        source_path = (script_dir / source_rel).resolve()\n        dest_path = (script_dir / dest_rel).resolve()\n        \n        # Check if source file exists\n        if not source_path.exists():\n            print(f\"Warning: Source file not found: {source_path}\")\n            continue\n        \n        # Ensure destination directory exists\n        dest_path.parent.mkdir(parents=True, exist_ok=True)\n        \n        # Copy the file\n        print(f\"Copying {source_path.name} -> {dest_path}\")\n        shutil.copy2(source_path, dest_path)\n        \n        # Apply patches if any are specified\n        if patches:\n            print(f\"Applying {len(patches)} patch(es) to {dest_path.name}\")\n            apply_patches(str(dest_path), patches)\n        else:\n            print(f\"No patches needed for {dest_path.name}\")\n\n\nif __name__ == '__main__':\n    copy_files()\n\n"
  },
  {
    "path": "Trading/Exchange/polymarket/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/polymarket_websocket_feed/__init__.py",
    "content": "from .polymarket_websocket import PolymarketWebsocketConnector\n"
  },
  {
    "path": "Trading/Exchange/polymarket_websocket_feed/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"PolymarketWebsocketConnector\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/polymarket_websocket_feed/polymarket_websocket.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\nfrom octobot_trading.enums import WebsocketFeeds as Feeds\nimport tentacles.Trading.Exchange.polymarket.polymarket_exchange as polymarket_exchange\n\n\nclass PolymarketWebsocketConnector(exchanges.CCXTWebsocketConnector):\n    EXCHANGE_FEEDS = {\n        Feeds.TRADES: True,\n        Feeds.KLINE: Feeds.UNSUPPORTED.value,\n        Feeds.TICKER: Feeds.UNSUPPORTED.value,\n        Feeds.CANDLE: Feeds.UNSUPPORTED.value,\n    }\n\n    @classmethod\n    def get_name(cls):\n        return polymarket_exchange.Polymarket.get_name()\n"
  },
  {
    "path": "Trading/Exchange/polymarket_websocket_feed/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/upbitexchange/__init__.py",
    "content": "from .upbit_exchange import UpbitExchange"
  },
  {
    "path": "Trading/Exchange/upbitexchange/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"UpbitExchange\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/upbitexchange/resources/upbitexchange.md",
    "content": "UpbitExchange is a basic RestExchange adaptation for WavesExchange exchange. \n"
  },
  {
    "path": "Trading/Exchange/upbitexchange/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/upbitexchange/upbit_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport octobot_trading.exchanges as exchanges\n\n\nclass UpbitExchange(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n\n    FIX_MARKET_STATUS = True\n\n    @classmethod\n    def get_name(cls):\n        return 'upbit'\n"
  },
  {
    "path": "Trading/Exchange/wavesexchange/__init__.py",
    "content": "from .wavesexchange_exchange import WavesExchange"
  },
  {
    "path": "Trading/Exchange/wavesexchange/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"WavesExchange\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Exchange/wavesexchange/resources/wavesexchange.md",
    "content": "WavesExchange is a basic RestExchange adaptation for WavesExchange exchange. \n"
  },
  {
    "path": "Trading/Exchange/wavesexchange/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Exchange/wavesexchange/wavesexchange_exchange.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport typing\n\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\nimport octobot_commons.enums as commons_enums\n\n\nclass WavesExchange(exchanges.RestExchange):\n    DESCRIPTION = \"\"\n    FIX_MARKET_STATUS = True\n    DUMP_INCOMPLETE_LAST_CANDLE = True  # set True in tentacle when the exchange can return incomplete last candles\n\n    @classmethod\n    def get_name(cls):\n        return 'wavesexchange'\n\n    def get_adapter_class(self):\n        return WavesCCXTAdapter\n\n    async def get_symbol_prices(self, symbol: str, time_frame: commons_enums.TimeFrames, limit: int = None,\n                                **kwargs: dict) -> typing.Optional[list]:\n        # without limit is not supported\n        if limit is not None:\n            # account for potentially dumped candle\n            limit += 1\n        return await super().get_symbol_prices(symbol, time_frame, limit=limit, **kwargs)\n\n\nclass WavesCCXTAdapter(exchanges.CCXTAdapter):\n\n    def fix_ticker(self, raw, **kwargs):\n        fixed = super().fix_ticker(raw, **kwargs)\n        fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \\\n            fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds()\n        return fixed\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/__init__.py",
    "content": "from .arbitrage_trading import ArbitrageTradingMode"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/arbitrage_container.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\n\n\nclass ArbitrageContainer:\n    # 0.3 %\n    SIMILARITY_RATIO = decimal.Decimal(str(0.003))\n\n    def __init__(self, own_exchange_price: decimal.Decimal, target_price: decimal.Decimal, state):\n        self.own_exchange_price: decimal.Decimal = own_exchange_price\n        self.target_price: decimal.Decimal = target_price\n        self.state = state\n        self.passed_initial_order = False\n        self.initial_before_fee_filled_quantity: decimal.Decimal = None\n        self.initial_limit_order_id = None\n        self.secondary_limit_order_id = None\n        self.secondary_stop_order_id = None\n\n    def is_similar(self, own_exchange_price: decimal.Decimal, state):\n        # if state and initial price is are the same or own_exchange_price is in current arbitrage window\n        return state is self.state and (\n            own_exchange_price == self.own_exchange_price\n            or (\n                (\n                    state is trading_enums.EvaluatorStates.LONG and\n                    (\n                            self.own_exchange_price * (trading_constants.ONE - ArbitrageContainer.SIMILARITY_RATIO)\n                            < own_exchange_price\n                            < self.target_price * (trading_constants.ONE + ArbitrageContainer.SIMILARITY_RATIO)\n                    )\n                )\n                or (\n                    state is trading_enums.EvaluatorStates.SHORT and\n                    (\n                            self.target_price * (trading_constants.ONE - ArbitrageContainer.SIMILARITY_RATIO)\n                            < own_exchange_price\n                            < self.own_exchange_price * (trading_constants.ONE + ArbitrageContainer.SIMILARITY_RATIO)\n                    )\n                )\n            )\n        )\n\n    def is_expired(self, other_exchanges_average_price):\n        if self.state is trading_enums.EvaluatorStates.LONG:\n            return other_exchanges_average_price < self.target_price * \\\n                   (trading_constants.ONE - ArbitrageContainer.SIMILARITY_RATIO)\n        if self.state is trading_enums.EvaluatorStates.SHORT:\n            return other_exchanges_average_price > self.target_price * \\\n                   (trading_constants.ONE + ArbitrageContainer.SIMILARITY_RATIO)\n\n    def should_be_discarded_after_order_cancel(self, order_id):\n        # should be discarded if initial order is cancelled\n        return self.initial_limit_order_id == order_id\n\n    def is_watching_this_order(self, order_id):\n        return self.initial_limit_order_id == order_id \\\n           or self.secondary_limit_order_id == order_id \\\n           or self.secondary_stop_order_id == order_id\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/arbitrage_trading.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport decimal\n\nimport async_channel.constants as channel_constants\nimport async_channel.channels as channel_instances\nimport octobot.constants as octobot_constants\nimport octobot_commons.data_util as data_util\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.pretty_printer as pretty_printer\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.octobot_channel_consumer as octobot_channel_consumer\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport tentacles.Trading.Mode.arbitrage_trading_mode.arbitrage_container as arbitrage_container_import\n\n\nclass ArbitrageTradingMode(trading_modes.AbstractTradingMode):\n\n    def __init__(self, config, exchange_manager):\n        super().__init__(config, exchange_manager)\n        self.merged_symbol = None\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.UI.user_input(\n            \"portfolio_percent_per_trade\", commons_enums.UserInputTypes.FLOAT, 25, inputs,\n            min_val=0, max_val=100,\n            title=\"Trade size: percent of your portfolio to include in each arbitrage order.\",\n        )\n        self.UI.user_input(\n            \"stop_loss_delta_percent\", commons_enums.UserInputTypes.FLOAT, 0.1, inputs,\n            min_val=0, max_val=100,\n            title=\"Stop loss price: price percent from the price of the initial order to set the stop loss on.\",\n        )\n        exchanges = list(self.config[commons_constants.CONFIG_EXCHANGES].keys())\n        self.UI.user_input(\n            \"exchanges_to_trade_on\", commons_enums.UserInputTypes.MULTIPLE_OPTIONS, [exchanges[0]], inputs,\n            options=exchanges,\n            title=\"Trading exchanges: exchanges on which to perform arbitrage trading: these will be used to create \"\n                  \"arbitrage orders. Leaving this empty will result in arbitrage trading on every exchange, \"\n                  \"which is sub-optimal. Add exchange configurations to add exchanges to this list.\",\n        )\n        self.UI.user_input(\n            \"minimal_price_delta_percent\", commons_enums.UserInputTypes.FLOAT, 0.25, inputs,\n            min_val=0, max_val=100,\n            title=\"Cross exchange triggering delta: minimal percent difference to trigger an arbitrage order. Remember \"\n                  \"to set it higher than twice your trading exchanges' fees since two orders will be placed each time.\",\n        )\n        self.UI.user_input(\n            \"enable_shorts\", commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=\"Enable shorts: enable arbitrage trades starting with a sell order and ending with a buy order.\",\n        )\n        self.UI.user_input(\n            \"enable_longs\", commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=\"Enable longs: enable arbitrage trades starting with a buy order and ending with a sell order.\",\n        )\n\n    def get_current_state(self) -> (str, float):\n        return super().get_current_state()[0] if self.producers[0].state is None else self.producers[0].state.name, \\\n               self.producers[0].final_eval if self.producers[0].final_eval else \"N/A\"\n\n    def get_mode_producer_classes(self) -> list:\n        return [ArbitrageModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [ArbitrageModeConsumer]\n\n    async def create_consumers(self) -> list:\n        consumers = await super().create_consumers()\n        # order consumer\n        order_consumer = await exchanges_channel.get_chan(trading_personal_data.OrdersChannel.get_name(),\n                                                          self.exchange_manager.id).new_consumer(\n            self._order_notification_callback,\n            symbol=self.symbol if self.symbol else channel_constants.CHANNEL_WILDCARD\n        )\n        return consumers + [order_consumer]\n\n    async def _order_notification_callback(self, exchange, exchange_id, cryptocurrency, symbol, order,\n                                           update_type, is_from_bot):\n        if order[\n            trading_enums.ExchangeConstantsOrderColumns.STATUS.value] == trading_enums.OrderStatus.FILLED.value \\\n                and is_from_bot:\n            await self.producers[0].order_filled_callback(order)\n        elif order[\n            trading_enums.ExchangeConstantsOrderColumns.STATUS.value] == trading_enums.OrderStatus.CANCELED.value \\\n                and is_from_bot:\n            await self.producers[0].order_cancelled_callback(order)\n\n    @classmethod\n    def get_is_trading_on_exchange(cls, exchange_name, tentacles_setup_config) -> bool:\n        \"\"\"\n        :return: True if exchange_name is in exchanges_to_trade_on (case insensitive)\n        or if exchanges_to_trade_on is missing or empty\n        \"\"\"\n        exchanges_to_trade_on = tentacles_manager_api.get_tentacle_config(tentacles_setup_config, cls) \\\n            .get(\"exchanges_to_trade_on\", [])\n        return not exchanges_to_trade_on or exchange_name.lower() in [\n            exchange.lower() for exchange in exchanges_to_trade_on\n        ]\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        \"\"\"\n        :return: True if the mode is not symbol dependant else False\n        \"\"\"\n        return False\n\n    @staticmethod\n    def is_backtestable():\n        return False\n\n\nclass ArbitrageModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    ARBITRAGE_CONTAINER_KEY = \"arbitrage\"\n    ARBITRAGE_PHASE_KEY = \"phase\"\n    QUANTITY_KEY = \"quantity\"\n    INITIAL_PHASE = \"initial\"\n    SECONDARY_PHASE = \"secondary\"\n\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self.open_arbitrages = []\n\n    def on_reload_config(self):\n        \"\"\"\n        Called at constructor and after the associated trading mode's reload_config.\n        Implement if necessary\n        \"\"\"\n        self.PORTFOLIO_PERCENT_PER_TRADE = decimal.Decimal(str(\n            self.trading_mode.trading_config[\"portfolio_percent_per_trade\"] / 100))\n        self.STOP_LOSS_DELTA_FROM_OWN_PRICE = decimal.Decimal(str(\n            self.trading_mode.trading_config[\"stop_loss_delta_percent\"] / 100))\n\n    async def create_new_orders(self, symbol, final_note, state, **kwargs):\n        # no possible default values in kwargs: interrupt if missing element\n        data = kwargs[self.CREATE_ORDER_DATA_PARAM]\n        phase = data[ArbitrageModeConsumer.ARBITRAGE_PHASE_KEY]\n        arbitrage_container = data[ArbitrageModeConsumer.ARBITRAGE_CONTAINER_KEY]\n        if phase == ArbitrageModeConsumer.INITIAL_PHASE:\n            await self._create_initial_arbitrage_order(arbitrage_container)\n        elif phase == ArbitrageModeConsumer.SECONDARY_PHASE:\n            await self._create_secondary_arbitrage_order(arbitrage_container, data[ArbitrageModeConsumer.QUANTITY_KEY])\n\n    async def _create_initial_arbitrage_order(self, arbitrage_container):\n        current_symbol_holding, current_market_holding, market_quantity, price, symbol_market = \\\n            await trading_personal_data.get_pre_order_data(self.exchange_manager,\n                                                           symbol=self.trading_mode.symbol,\n                                                           timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n\n        created_orders = []\n        order_type = trading_enums.TraderOrderType.BUY_LIMIT \\\n            if arbitrage_container.state is trading_enums.EvaluatorStates.LONG \\\n            else trading_enums.TraderOrderType.SELL_LIMIT\n        quantity = self._get_quantity_from_holdings(current_symbol_holding, market_quantity, arbitrage_container.state)\n        if order_type is trading_enums.TraderOrderType.SELL_LIMIT:\n            quantity = trading_personal_data.decimal_add_dusts_to_quantity_if_necessary(quantity, price, symbol_market,\n                                                                                        current_symbol_holding)\n        for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                quantity,\n                arbitrage_container.own_exchange_price,\n                symbol_market):\n            current_order = trading_personal_data.create_order_instance(trader=self.exchange_manager.trader,\n                                                                        order_type=order_type,\n                                                                        symbol=self.trading_mode.symbol,\n                                                                        current_price=arbitrage_container.own_exchange_price,\n                                                                        quantity=order_quantity,\n                                                                        price=order_price)\n            created_order = await self.trading_mode.create_order(current_order)\n            if created_order is not None:\n                created_orders.append(created_order)\n                arbitrage_container.initial_limit_order_id = created_order.order_id\n                self.open_arbitrages.append(arbitrage_container)\n            # only create one order per arbitrage\n            return created_orders\n\n    async def _create_secondary_arbitrage_order(self, arbitrage_container, quantity):\n        created_orders = []\n        current_symbol_holding, current_market_holding, market_quantity, price, symbol_market = \\\n            await trading_personal_data.get_pre_order_data(self.exchange_manager,\n                                                           symbol=self.trading_mode.symbol,\n                                                           timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n        now_selling = arbitrage_container.state is trading_enums.EvaluatorStates.LONG\n        entry_id = arbitrage_container.initial_limit_order_id\n        if now_selling:\n            quantity = trading_personal_data.decimal_add_dusts_to_quantity_if_necessary(quantity, price, symbol_market,\n                                                                                        current_symbol_holding)\n        for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n            quantity,\n            arbitrage_container.target_price,\n            symbol_market\n        ):\n            oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(\n                trading_personal_data.OneCancelsTheOtherOrderGroup,\n                active_order_swap_strategy=trading_personal_data.StopFirstActiveOrderSwapStrategy()\n            )\n            current_limit_order = trading_personal_data.create_order_instance(\n                trader=self.exchange_manager.trader,\n                order_type=trading_enums.TraderOrderType.SELL_LIMIT if now_selling\n                else trading_enums.TraderOrderType.BUY_LIMIT,\n                symbol=self.trading_mode.symbol,\n                current_price=arbitrage_container.own_exchange_price,\n                quantity=order_quantity,\n                price=order_price,\n                group=oco_group,\n                associated_entry_id=entry_id\n            )\n            stop_price = self._get_stop_loss_price(symbol_market,\n                                                   arbitrage_container.own_exchange_price,\n                                                   now_selling)\n            current_stop_order = trading_personal_data.create_order_instance(\n                trader=self.exchange_manager.trader,\n                order_type=trading_enums.TraderOrderType.STOP_LOSS,\n                symbol=self.trading_mode.symbol,\n                current_price=arbitrage_container.own_exchange_price,\n                quantity=order_quantity,\n                price=stop_price,\n                group=oco_group,\n                side=trading_enums.TradeOrderSide.SELL\n                if now_selling else trading_enums.TradeOrderSide.BUY,\n                associated_entry_id=entry_id,\n            )\n            # in futures, inactive orders are not necessary\n            if self.exchange_manager.trader.enable_inactive_orders and not self.exchange_manager.is_future:\n                await oco_group.active_order_swap_strategy.apply_inactive_orders(\n                    [current_limit_order, current_stop_order]\n                )\n            if created_limit_order := await self.trading_mode.create_order(current_limit_order):\n                created_stop_order = await self.trading_mode.create_order(current_stop_order)\n                created_orders.append(created_limit_order)\n                arbitrage_container.secondary_limit_order_id = created_limit_order.order_id\n                arbitrage_container.secondary_stop_order_id = created_stop_order.order_id\n            return created_orders\n        return []\n\n    def _get_quantity_from_holdings(self, current_symbol_holding, market_quantity, state):\n        # TODO handle quantity in a non dynamic manner (avoid subsequent orders volume reduction)\n        if state is trading_enums.EvaluatorStates.LONG:\n            return market_quantity * self.PORTFOLIO_PERCENT_PER_TRADE\n        return current_symbol_holding * self.PORTFOLIO_PERCENT_PER_TRADE\n\n    def _get_stop_loss_price(self, symbol_market, starting_price, now_selling):\n        if now_selling:\n            return trading_personal_data.decimal_adapt_price(symbol_market,\n                                                             starting_price * (trading_constants.ONE\n                                                                               - self.STOP_LOSS_DELTA_FROM_OWN_PRICE))\n        return trading_personal_data.decimal_adapt_price(symbol_market,\n                                                         starting_price * (trading_constants.ONE\n                                                                           + self.STOP_LOSS_DELTA_FROM_OWN_PRICE))\n\n\nclass ArbitrageModeProducer(trading_modes.AbstractTradingModeProducer):\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        self.own_exchange_mark_price: decimal.Decimal = None\n        self.other_exchanges_mark_prices = {}\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n        self.final_eval = \"\"\n        self.quote, self.base = symbol_util.parse_symbol(self.trading_mode.symbol).base_and_quote()\n        self.lock = asyncio.Lock()\n        self.enable_shorts = self.enable_longs = True\n\n    def on_reload_config(self):\n        \"\"\"\n        Called at constructor and after the associated trading mode's reload_config.\n        Implement if necessary\n        \"\"\"\n        self.sup_triggering_price_delta_ratio: decimal.Decimal = \\\n            1 + decimal.Decimal(str(self.trading_mode.trading_config[\"minimal_price_delta_percent\"] / 100))\n        self.inf_triggering_price_delta_ratio: decimal.Decimal = \\\n            1 - decimal.Decimal(str(self.trading_mode.trading_config[\"minimal_price_delta_percent\"] / 100))\n        self.enable_shorts = self.trading_mode.trading_config.get(\"enable_shorts\", True)\n        self.enable_longs = self.trading_mode.trading_config.get(\"enable_longs\", True)\n\n    async def inner_start(self) -> None:\n        \"\"\"\n        Start trading mode channels subscriptions\n        \"\"\"\n        try:\n            self.logger.info(f\"Starting on listening for {self.trading_mode.symbol} arbitrage opportunities on \"\n                             f\"{self.exchange_name} based on other exchanges prices.\")\n            for exchange_id in trading_api.get_all_exchange_ids_with_same_matrix_id(self.exchange_manager.exchange_name,\n                                                                                    self.exchange_manager.id):\n                # subscribe on existing exchanges\n                if exchange_id != self.exchange_manager.id:\n                    await self._subscribe_exchange_id_mark_price(exchange_id)\n            await exchanges_channel.get_chan(trading_constants.MARK_PRICE_CHANNEL, self.exchange_manager.id). \\\n                new_consumer(\n                self._own_exchange_mark_price_callback,\n                symbol=self.trading_mode.symbol\n            )\n            await channel_instances.get_chan_at_id(octobot_constants.OCTOBOT_CHANNEL, self.trading_mode.bot_id). \\\n                new_consumer(\n                # listen for new available exchange\n                self._exchange_added_callback,\n                subject=commons_enums.OctoBotChannelSubjects.NOTIFICATION.value,\n                action=octobot_channel_consumer.OctoBotChannelTradingActions.EXCHANGE.value\n            )\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when starting arbitrage trading on {self.exchange_name}: {e}\")\n\n    async def order_filled_callback(self, filled_order):\n        \"\"\"\n        Called when an order is filled: create secondary orders if the filled order is an initial order\n        :param filled_order:\n        :return: None\n        \"\"\"\n        order_id = filled_order[trading_enums.ExchangeConstantsOrderColumns.ID.value]\n        async with self.lock:\n            arbitrage = self._get_arbitrage(order_id)\n            if arbitrage is not None:\n                filled_quantity = filled_order[trading_enums.ExchangeConstantsOrderColumns.FILLED.value]\n                if arbitrage.passed_initial_order:\n                    # filled limit order or stop loss: close arbitrage\n                    arbitrage_success = filled_order[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] != \\\n                                        trading_enums.TradeOrderType.STOP_LOSS.value\n                    if arbitrage.state is trading_enums.EvaluatorStates.LONG:\n                        filled_quantity = decimal.Decimal(str(filled_quantity * filled_order[\n                            trading_enums.ExchangeConstantsOrderColumns.PRICE.value]))\n                    self._log_results(arbitrage, arbitrage_success, filled_quantity)\n                    self._close_arbitrage(arbitrage)\n                else:\n                    await self._trigger_arbitrage_secondary_order(arbitrage, filled_order, filled_quantity)\n\n    async def order_cancelled_callback(self, cancelled_order):\n        \"\"\"\n        Called when an order is cancelled (from bot or user)\n        :param cancelled_order: the cancelled order\n        :return: None\n        \"\"\"\n        order_id = cancelled_order[trading_enums.ExchangeConstantsOrderColumns.ID.value]\n        async with self.lock:\n            to_remove_orders = [arbitrage\n                                for arbitrage in self._get_open_arbitrages()\n                                if arbitrage.should_be_discarded_after_order_cancel(order_id)]\n            for arbitrage in to_remove_orders:\n                self._close_arbitrage(arbitrage)\n\n    async def _own_exchange_mark_price_callback(\n            self, exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mark_price\n    ):\n        \"\"\"\n        Called on a price update from the current exchange\n        :param exchange: name of the exchange\n        :param exchange_id: id of the exchange\n        :param cryptocurrency: related cryptocurrency\n        :param symbol: related symbol\n        :param mark_price: updated mark price\n        :return: None\n        \"\"\"\n        self.own_exchange_mark_price = decimal.Decimal(str(mark_price))\n        try:\n            if self.other_exchanges_mark_prices:\n                await self._analyse_arbitrage_opportunities()\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when handling mark_price_callback for {self.exchange_name}: {e}\")\n\n    async def _mark_price_callback(\n            self, exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mark_price\n    ):\n        \"\"\"\n        Called on a price update from an exchange that is different from the current one\n        :param exchange: name of the exchange\n        :param exchange_id: id of the exchange\n        :param cryptocurrency: related cryptocurrency\n        :param symbol: related symbol\n        :param mark_price: updated mark price\n        :return: None\n        \"\"\"\n        self.other_exchanges_mark_prices[exchange] = decimal.Decimal(str(mark_price))\n        try:\n            if self.own_exchange_mark_price is not None:\n                await self._analyse_arbitrage_opportunities()\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when handling mark_price_callback for {self.exchange_name}: {e}\")\n\n    async def _analyse_arbitrage_opportunities(self):\n        async with self.trading_mode_trigger():\n            other_exchanges_average_price = \\\n                decimal.Decimal(str(data_util.mean(self.other_exchanges_mark_prices.values())))\n            state = None\n            if other_exchanges_average_price > self.own_exchange_mark_price * self.sup_triggering_price_delta_ratio:\n                # min long = high price > own_price / (1 - 2fees)\n                state = trading_enums.EvaluatorStates.LONG\n            elif other_exchanges_average_price < self.own_exchange_mark_price * self.inf_triggering_price_delta_ratio:\n                # min short = low price < own_price * (1 - 2fees)\n                state = trading_enums.EvaluatorStates.SHORT\n            if self._is_traded_state(state):\n                # lock to prevent concurrent order management\n                async with self.lock:\n                    # 1. cancel invalided opportunities if any\n                    await self._ensure_no_expired_opportunities(other_exchanges_average_price, state)\n                    # 2. handle new opportunities\n                    await self._trigger_arbitrage_opportunity(other_exchanges_average_price, state)\n\n    def _is_traded_state(self, state):\n        if state is None:\n            return False\n        if state is trading_enums.EvaluatorStates.SHORT:\n            return self.enable_shorts\n        if state is trading_enums.EvaluatorStates.LONG:\n            return self.enable_longs\n\n    async def _trigger_arbitrage_opportunity(self, other_exchanges_average_price, state):\n        # ensure no similar arbitrage is already in place\n        if self._ensure_no_existing_arbitrage_on_this_price(state):\n            self._log_arbitrage_opportunity_details(other_exchanges_average_price, state)\n            arbitrage_container = arbitrage_container_import.ArbitrageContainer(self.own_exchange_mark_price,\n                                                                                other_exchanges_average_price, state)\n            await self._create_arbitrage_initial_order(arbitrage_container)\n            self._register_state(state, other_exchanges_average_price - self.own_exchange_mark_price)\n\n    async def _create_arbitrage_initial_order(self, arbitrage_container):\n        if self.exchange_manager.trader.is_enabled:\n            data = {\n                ArbitrageModeConsumer.ARBITRAGE_CONTAINER_KEY: arbitrage_container,\n                ArbitrageModeConsumer.ARBITRAGE_PHASE_KEY: ArbitrageModeConsumer.INITIAL_PHASE\n            }\n            await self.submit_trading_evaluation(cryptocurrency=self.trading_mode.cryptocurrency,\n                                                 symbol=self.trading_mode.symbol,\n                                                 time_frame=None,\n                                                 state=arbitrage_container.state,\n                                                 data=data)\n\n    async def _trigger_arbitrage_secondary_order(self, arbitrage: arbitrage_container_import.ArbitrageContainer,\n                                                 filled_order: dict,\n                                                 filled_quantity_before_fees: decimal.Decimal):\n        arbitrage.passed_initial_order = True\n        now_buying = arbitrage.state is trading_enums.EvaluatorStates.SHORT\n        # a SHORT arbitrage is an initial SELL followed by a BUY order.\n        # Here in the secondary order construction:\n        # - Buy (at a lower price) when the arbitrage is a SHORT\n        # - Sell (at a higher price) when the arbitrage is a LONG\n        paid_fees_in_quote = trading_personal_data.total_fees_from_order_dict(filled_order, self.quote)\n        secondary_quantity = filled_quantity_before_fees - paid_fees_in_quote\n        filled_price = decimal.Decimal(str(filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]))\n        if now_buying:\n            arbitrage.initial_before_fee_filled_quantity = filled_quantity_before_fees\n        else:\n            arbitrage.initial_before_fee_filled_quantity = filled_quantity_before_fees * filled_price\n        if now_buying:\n            # buying at a lower price: buy more than what has been sold, take fees into account\n            fees_in_base = trading_personal_data.total_fees_from_order_dict(filled_order, self.base)\n            secondary_base_amount = filled_price * secondary_quantity - fees_in_base\n            secondary_quantity = secondary_base_amount / arbitrage.target_price\n        await self._create_arbitrage_secondary_order(arbitrage, secondary_quantity)\n\n    async def _create_arbitrage_secondary_order(self, arbitrage_container, secondary_quantity):\n        if self.exchange_manager.trader.is_enabled:\n            data = {\n                ArbitrageModeConsumer.ARBITRAGE_CONTAINER_KEY: arbitrage_container,\n                ArbitrageModeConsumer.ARBITRAGE_PHASE_KEY: ArbitrageModeConsumer.SECONDARY_PHASE,\n                ArbitrageModeConsumer.QUANTITY_KEY: secondary_quantity\n            }\n            await self.submit_trading_evaluation(cryptocurrency=self.trading_mode.cryptocurrency,\n                                                 symbol=self.trading_mode.symbol,\n                                                 time_frame=None,\n                                                 state=arbitrage_container.state,\n                                                 data=data)\n\n    def _ensure_no_existing_arbitrage_on_this_price(self, state):\n        for arbitrage_container in self._get_open_arbitrages():\n            if arbitrage_container.is_similar(self.own_exchange_mark_price, state):\n                return False\n        return True\n\n    def _get_arbitrage(self, order_id):\n        for arbitrage_container in self._get_open_arbitrages():\n            if arbitrage_container.is_watching_this_order(order_id):\n                return arbitrage_container\n        return None\n\n    async def _ensure_no_expired_opportunities(self, other_exchanges_average_price, state):\n        to_remove_arbitrages = []\n        for arbitrage_container in self._get_open_arbitrages():\n            # look for expired opposite side arbitrages and cancel them if still possible\n            if arbitrage_container.state is not state and \\\n                    arbitrage_container.is_expired(other_exchanges_average_price):\n                if self.exchange_manager.trader.is_enabled:\n                    if await self._cancel_order(arbitrage_container):\n                        to_remove_arbitrages.append(arbitrage_container)\n\n        for arbitrage in to_remove_arbitrages:\n            self._get_open_arbitrages().remove(arbitrage)\n\n    async def _cancel_order(self, arbitrage_container) -> bool:\n        try:\n            if await self.trading_mode.cancel_order(\n                self.exchange_manager.exchange_personal_data.orders_manager.get_order(\n                    arbitrage_container.initial_limit_order_id\n                )\n            ):\n                self.logger.info(f\"Arbitrage opportunity expired: cancelled initial order on \"\n                                 f\"{self.exchange_manager.exchange_name} for {self.trading_mode.symbol} at\"\n                                 f\"{arbitrage_container.own_exchange_price}\")\n                return True\n            return False\n        except (trading_errors.OrderCancelError, trading_errors.UnexpectedExchangeSideOrderStateError) as err:\n            self.logger.warning(f\"Skipping order cancel: {err}\")\n            # order can't be cancelled, ignore it and proceed\n            return True\n        except KeyError:\n            # order is not open anymore: can't cancel\n            return False\n\n    def _log_arbitrage_opportunity_details(self, other_exchanges_average_price, state):\n        price_difference = other_exchanges_average_price / self.own_exchange_mark_price\n        difference_percent = pretty_printer.round_with_decimal_count(float(price_difference) * 100 - 100, 5)\n        self.logger.debug(f\"Arbitrage opportunity on {self.exchange_manager.exchange_name} {state.name} for \"\n                          f\"{self.trading_mode.symbol} \"\n                          f\"({str(self.own_exchange_mark_price)} vs {other_exchanges_average_price} on average \"\n                          f\"based on {len(self.other_exchanges_mark_prices)} registered exchange(s): \"\n                          f\"{'+' if price_difference > 1 else ''}{difference_percent}%).\")\n\n    def _log_results(self, arbitrage, success, filled_quantity):\n        self.logger.info(f\"Closed {arbitrage.state.name} arbitrage on {self.exchange_manager.exchange_name} [\"\n                         f\"{'success' if success else 'stop loss triggered'}] with {self.trading_mode.symbol}: \"\n                         f\"profit before {'final' if arbitrage.state is trading_enums.EvaluatorStates.SHORT else 'all'} \"\n                         f\"fees: {str(filled_quantity - arbitrage.initial_before_fee_filled_quantity)} \"\n                         f\"{self.quote if arbitrage.state is trading_enums.EvaluatorStates.SHORT else self.base}\")\n\n    def _close_arbitrage(self, arbitrage):\n        self._get_open_arbitrages().remove(arbitrage)\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n        self.final_eval = \"\"\n\n    def _get_open_arbitrages(self):\n        return self.trading_mode.get_trading_mode_consumers()[0].open_arbitrages\n\n    def _register_state(self, new_state, price_difference):\n        self.state = new_state\n        self.final_eval = f\"{'+' if float(price_difference) > 0 else ''}{str(price_difference)}\"\n        self.logger.info(f\"New state on {self.exchange_manager.exchange_name} for {self.trading_mode.symbol}: \"\n                         f\"{new_state}, price difference: {self.final_eval}\")\n\n    async def _exchange_added_callback(self, bot_id: str, subject: str, action: str, data: dict):\n        if octobot_channel_consumer.OctoBotChannelTradingDataKeys.EXCHANGE_ID.value in data:\n            # New exchange available: subscribe to its price updates\n            await self._subscribe_exchange_id_mark_price(\n                data[octobot_channel_consumer.OctoBotChannelTradingDataKeys.EXCHANGE_ID.value])\n\n    async def _subscribe_exchange_id_mark_price(self, exchange_id):\n        await exchanges_channel.get_chan(trading_constants.MARK_PRICE_CHANNEL, exchange_id).new_consumer(\n            self._mark_price_callback,\n            symbol=self.trading_mode.symbol\n        )\n        registered_exchange_name = trading_api.get_exchange_name(\n            trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n        )\n        self.logger.info(\n            f\"Arbitrage trading for {self.trading_mode.symbol} on {self.exchange_name}: registered \"\n            f\"{registered_exchange_name} exchange as price data feed reference to identify arbitrage opportunities.\"\n        )\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        # Ignore matrix calls\n        pass\n\n    @classmethod\n    def get_should_cancel_loaded_orders(cls) -> bool:\n        return False\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        await super().stop()\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/config/ArbitrageTradingMode.json",
    "content": "{\n    \"minimal_price_delta_percent\": 0.25,\n    \"portfolio_percent_per_trade\": 25,\n    \"stop_loss_delta_percent\": 0.1,\n    \"exchanges_to_trade_on\": [],\n    \"required_strategies\": []\n}"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"ArbitrageTradingMode\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/resources/ArbitrageTradingMode.md",
    "content": "ArbitrageTradingMode is watching prices of the configured trading pairs across the available exchanges \nto find [arbitrage](https://www.investopedia.com/terms/a/arbitrage.asp) opportunities.\n\nArbitrageTradingMode is watching the price of the traded pairs accord every exchange and computes its average price.\nIf the price of a pair is far enough from its average cross-exchange price, an arbitrage trade is initiated. \n\nAn arbitrage trade consists in **2 orders**:\n 1. A limit buy or sell at the current local exchange price\n 2. When this first order is filled:\n    - A limit buy or a sell at the average price (average of prices on other exchanges) is created to benefit from the arbitrage opportunity \n    - A stop loss on the opposite side is created to secure funds\n    \nThe first limit order is cancelled if the local exchange price reaches the other exchanges average price.  \n**No funds are transferred** from one exchange to another, it all happens on the same exchange.\n\nIt is recommended to enable arbitrage trading on **few exchanges only** to benefit from **price lag**: \nsimply register these exchanges in your ArbitrageTradingMode configuration.  \n**Every exchange** in your OctoBot configuration will be used to compute the **average price** for each traded pair, \ntherefore you can add **highly liquid exchanges** to be used as **price references only** and quickly \nspot arbitrage opportunities.\n\nBy default **every exchange** in your OctoBot configuration is used for arbitrage trading. It is recommended to \n**narrow this list down** in your ArbitrageTradingMode configuration and **only trade on the ones offering \narbitrage opportunities and use the others as price indicators**.\n\nExchanges that are used for **price reference only require no api keys** as no trade is performed on these exchanges.\n\n<div class=\"text-center\">\n    <img src=\"https://raw.githubusercontent.com/Drakkar-Software/OctoBot/assets/arbitrage.png\" width=\"100%\" height=\"100%\">\n</div>\n\n_This trading mode supports PNL history._\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport contextlib\nimport os.path\nimport decimal\nimport mock\n\nimport octobot_backtesting.api as backtesting_api\nimport async_channel.util as channel_util\nimport octobot_commons.tests.test_config as test_config\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchanges as exchanges\nimport tentacles.Trading.Mode as modes\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\nimport tests.test_utils.trading_modes as test_trading_modes\nfrom tentacles.Trading.Mode.arbitrage_trading_mode.arbitrage_trading import ArbitrageModeProducer\n\n\n@contextlib.asynccontextmanager\nasync def exchange(exchange_name, backtesting=None, symbol=\"BTC/USDT\"):\n    exchange_manager = None\n    try:\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, exchange_name)\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = backtesting or await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[os.path.join(test_config.TEST_CONFIG_FOLDER,\n                                     \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel,\n                                            exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        mode = modes.ArbitrageTradingMode(config, exchange_manager)\n        mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n        # avoid error with producer init and exchanges keys\n        with mock.patch.object(ArbitrageModeProducer, \"start\", new=mock.AsyncMock()) as start_mock:\n            await mode.initialize()\n            # add mode to exchange manager so that it can be stopped and freed from memory\n            exchange_manager.trading_modes.append(mode)\n\n            # set BTC/USDT price at 1000 USDT\n            trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n            # force triggering_price_delta_ratio equivalent to a 0.2% setting in minimal_price_delta_percent\n            delta_percent = 2\n            test_trading_modes.set_ready_to_start(mode.producers[0])\n            mode.producers[0].inf_triggering_price_delta_ratio = decimal.Decimal(str(1 - delta_percent / 100))\n            mode.producers[0].sup_triggering_price_delta_ratio = decimal.Decimal(str(1 + delta_percent / 100))\n            # let trading modes start\n            await asyncio_tools.wait_asyncio_next_cycle()\n            start_mock.assert_called_once()\n        yield mode.producers[0], mode.get_trading_mode_consumers()[0], exchange_manager\n    finally:\n        if exchange_manager is not None:\n            for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n                await backtesting_api.stop_importer(importer)\n            if exchange_manager.exchange.backtesting.time_updater is not None:\n                await exchange_manager.exchange.backtesting.stop()\n            await exchange_manager.stop()\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/tests/test_arbitrage_container.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\n\nimport octobot_trading.enums as trading_enums\nimport tentacles.Trading.Mode.arbitrage_trading_mode.arbitrage_container as arbitrage_container_import\n\n\ndef test_is_similar_with_prices_close_to_own_price():\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(90), decimal.Decimal(100),\n                                                              trading_enums.EvaluatorStates.LONG)\n    # same price and state\n    assert container.is_similar(decimal.Decimal(90), trading_enums.EvaluatorStates.LONG)\n    # same price but different state\n    assert not container.is_similar(decimal.Decimal(90), trading_enums.EvaluatorStates.SHORT)\n\n    # too different prices comparing to own_exchange_price\n    for price in (decimal.Decimal(110), decimal.Decimal(200), decimal.Decimal(80),\n                  decimal.Decimal(20), decimal.Decimal(0)):\n        assert not container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert not container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n    # similar prices comparing to own_exchange_price\n    for price in (decimal.Decimal(str(89.97)), decimal.Decimal(str(90.01))):\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n\ndef test_is_similar_with_prices_close_to_own_price_very_low_prices():\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(0.00000621)),\n                                                              decimal.Decimal(str(0.00000645)),\n                                                              trading_enums.EvaluatorStates.LONG)\n\n    # too different prices comparing to own_exchange_price\n    for price in (decimal.Decimal(str(0.0000060)), decimal.Decimal(str(0.0000061)), decimal.Decimal(str(0.0000065)),\n                  decimal.Decimal(str(0.000007))):\n        assert not container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert not container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n    # similar prices comparing to own_exchange_price\n    for price in (decimal.Decimal(str(0.000006196)), decimal.Decimal(str(0.00000620)), decimal.Decimal(str(0.00000646)),\n                  decimal.Decimal(str(0.000006463))):\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(0.00000062)),\n                                                              decimal.Decimal(str(0.00000064)),\n                                                              trading_enums.EvaluatorStates.LONG)\n\n    # too different prices comparing to own_exchange_price\n    for price in (decimal.Decimal(str(0.00000060)), decimal.Decimal(str(0.00000061)), decimal.Decimal(str(0.00000065)),\n                  decimal.Decimal(str(0.0000007))):\n        assert not container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert not container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n    # similar prices comparing to own_exchange_price\n    for price in (decimal.Decimal(str(0.0000006199)), decimal.Decimal(str(0.0000006401))):\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n\ndef test_is_similar_with_prices_in_arbitrage_range():\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(90), decimal.Decimal(100),\n                                                              trading_enums.EvaluatorStates.LONG)\n\n    for price in range(int(container.own_exchange_price), int(container.target_price)):\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n        assert container.is_similar(price, trading_enums.EvaluatorStates.LONG)\n\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(100), decimal.Decimal(90),\n                                                              trading_enums.EvaluatorStates.SHORT)\n\n    for price in range(int(container.target_price), int(container.own_exchange_price)):\n        assert container.is_similar(price, trading_enums.EvaluatorStates.SHORT)\n        assert container.is_similar(price, trading_enums.EvaluatorStates.SHORT)\n\n\ndef test_is_expired():\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(90), decimal.Decimal(100),\n                                                              trading_enums.EvaluatorStates.LONG)\n    assert not container.is_expired(decimal.Decimal(99.99))\n    assert container.is_expired(decimal.Decimal(99))\n\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(100), decimal.Decimal(90),\n                                                              trading_enums.EvaluatorStates.SHORT)\n    assert not container.is_expired(decimal.Decimal(90.01))\n    assert container.is_expired(decimal.Decimal(91))\n\n\ndef test_is_expired_very_low_prices():\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(0.00000621)),\n                                                              decimal.Decimal(str(0.00000645)),\n                                                              trading_enums.EvaluatorStates.LONG)\n    assert not container.is_expired(decimal.Decimal(str(0.00000644)))\n    assert container.is_expired(decimal.Decimal(str(0.00000643)))\n\n    container = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(0.00000062)),\n                                                              decimal.Decimal(str(0.00000064)),\n                                                              trading_enums.EvaluatorStates.LONG)\n    assert not container.is_expired(decimal.Decimal(str(0.000000639)))\n    assert container.is_expired(decimal.Decimal(str(0.000000637)))\n\n\ndef test_should_be_discarded_after_order_cancel():\n    container = arbitrage_container_import.ArbitrageContainer(90, 100, trading_enums.EvaluatorStates.LONG)\n    assert not container.should_be_discarded_after_order_cancel(\"123\")\n    container.initial_limit_order_id = \"123\"\n    assert container.should_be_discarded_after_order_cancel(\"123\")\n    assert not container.should_be_discarded_after_order_cancel(\"1234\")\n\n\ndef test_is_watching_this_order():\n    container = arbitrage_container_import.ArbitrageContainer(90, 100, trading_enums.EvaluatorStates.LONG)\n    assert not container.is_watching_this_order(\"init\")\n    assert not container.is_watching_this_order(\"sec\")\n    assert not container.is_watching_this_order(\"stop\")\n\n    container.initial_limit_order_id = \"init\"\n    assert container.is_watching_this_order(\"init\")\n    assert not container.is_watching_this_order(\"sec\")\n    assert not container.is_watching_this_order(\"stop\")\n\n    container.secondary_limit_order_id = \"sec\"\n    assert container.is_watching_this_order(\"init\")\n    assert container.is_watching_this_order(\"sec\")\n    assert not container.is_watching_this_order(\"stop\")\n\n    container.secondary_stop_order_id = \"stop\"\n    assert container.is_watching_this_order(\"init\")\n    assert container.is_watching_this_order(\"sec\")\n    assert container.is_watching_this_order(\"stop\")\n\n    container.initial_limit_order_id = None\n    assert not container.is_watching_this_order(\"init\")\n    assert container.is_watching_this_order(\"sec\")\n    assert container.is_watching_this_order(\"stop\")\n\n    container.secondary_limit_order_id = None\n    assert not container.is_watching_this_order(\"init\")\n    assert not container.is_watching_this_order(\"sec\")\n    assert container.is_watching_this_order(\"stop\")\n\n    container.secondary_stop_order_id = None\n    assert not container.is_watching_this_order(\"init\")\n    assert not container.is_watching_this_order(\"sec\")\n    assert not container.is_watching_this_order(\"stop\")\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/tests/test_arbitrage_trading_mode_consumer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport mock\nimport decimal\n\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport tentacles.Trading.Mode.arbitrage_trading_mode.arbitrage_container as arbitrage_container_import\nimport tentacles.Trading.Mode.arbitrage_trading_mode.arbitrage_trading as arbitrage_trading_mode\nimport tentacles.Trading.Mode.arbitrage_trading_mode.tests as arbitrage_trading_mode_tests\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_init():\n    tentacles_manager_api.reload_tentacle_info()\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as arbitrage_trading_mode_tests.exchange_tuple:\n        binance_producer, binance_consumer, _ = arbitrage_trading_mode_tests.exchange_tuple\n\n        # trading mode\n        assert len(binance_producer.trading_mode.consumers) == 2\n        assert len(binance_producer.trading_mode.producers) == 1\n\n        # consumer\n        assert binance_consumer.PORTFOLIO_PERCENT_PER_TRADE > trading_constants.ZERO\n        assert binance_consumer.STOP_LOSS_DELTA_FROM_OWN_PRICE > trading_constants.ZERO\n        assert binance_consumer.open_arbitrages == []\n\n\nasync def test_create_new_orders():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as arbitrage_trading_mode_tests.exchange_tuple:\n        _, binance_consumer, _ = arbitrage_trading_mode_tests.exchange_tuple\n\n        symbol = \"BTC/USDT\"\n        final_note = None\n        state = trading_enums.EvaluatorStates.SHORT\n        with mock.patch.object(binance_consumer, \"_create_initial_arbitrage_order\",\n                               new=mock.AsyncMock()) as initial_mock, \\\n                mock.patch.object(binance_consumer, \"_create_secondary_arbitrage_order\",\n                                  new=mock.AsyncMock()) as secondary_mock:\n            # no data in kwargs\n            with pytest.raises(KeyError):\n                await binance_consumer.create_new_orders(symbol, final_note, state)\n            initial_mock.assert_not_called()\n            secondary_mock.assert_not_called()\n\n            data = {\n                arbitrage_trading_mode.ArbitrageModeConsumer.ARBITRAGE_PHASE_KEY: arbitrage_trading_mode.ArbitrageModeConsumer.INITIAL_PHASE,\n                arbitrage_trading_mode.ArbitrageModeConsumer.ARBITRAGE_CONTAINER_KEY: None\n            }\n            await binance_consumer.create_new_orders(symbol, final_note, state, data=data)\n            initial_mock.assert_called_once()\n            secondary_mock.assert_not_called()\n            initial_mock.reset_mock()\n\n            data = {\n                arbitrage_trading_mode.ArbitrageModeConsumer.ARBITRAGE_PHASE_KEY: arbitrage_trading_mode.ArbitrageModeConsumer.SECONDARY_PHASE,\n                arbitrage_trading_mode.ArbitrageModeConsumer.ARBITRAGE_CONTAINER_KEY: None,\n                arbitrage_trading_mode.ArbitrageModeConsumer.QUANTITY_KEY: None\n            }\n            await binance_consumer.create_new_orders(symbol, final_note, state, data=data)\n            initial_mock.assert_not_called()\n            secondary_mock.assert_called_once()\n\n\nasync def test_create_initial_arbitrage_order():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as arbitrage_trading_mode_tests.exchange_tuple:\n        _, binance_consumer, _ = arbitrage_trading_mode_tests.exchange_tuple\n        price = decimal.Decimal(10)\n        # long\n        arbitrage = arbitrage_container_import.ArbitrageContainer(price, decimal.Decimal(15), trading_enums.EvaluatorStates.LONG)\n        orders = await binance_consumer._create_initial_arbitrage_order(arbitrage)\n        assert orders\n        order = orders[0]\n        assert order.exchange_order_type is trading_enums.TradeOrderType.LIMIT\n        assert order.order_type is trading_enums.TraderOrderType.BUY_LIMIT\n        assert order.side is trading_enums.TradeOrderSide.BUY\n        assert order.symbol == binance_consumer.trading_mode.symbol\n        assert order.order_id == arbitrage.initial_limit_order_id\n        assert arbitrage in binance_consumer.open_arbitrages\n\n        # short\n        arbitrage = arbitrage_container_import.ArbitrageContainer(price, decimal.Decimal(15), trading_enums.EvaluatorStates.SHORT)\n        orders = await binance_consumer._create_initial_arbitrage_order(arbitrage)\n        assert orders\n        order = orders[0]\n        assert order.exchange_order_type is trading_enums.TradeOrderType.LIMIT\n        assert order.order_type is trading_enums.TraderOrderType.SELL_LIMIT\n        assert order.side is trading_enums.TradeOrderSide.SELL\n        assert order.symbol == binance_consumer.trading_mode.symbol\n        assert order.order_id == arbitrage.initial_limit_order_id\n        assert arbitrage in binance_consumer.open_arbitrages\n\n\nasync def test_create_secondary_arbitrage_order():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as arbitrage_trading_mode_tests.exchange_tuple:\n        _, binance_consumer, exchange_manager = arbitrage_trading_mode_tests.exchange_tuple\n        price = decimal.Decimal(10)\n\n        # disable inactive orders\n        exchange_manager.trader.enable_inactive_orders = False\n        # long\n        arbitrage = arbitrage_container_import.ArbitrageContainer(\n            price, decimal.Decimal(15), trading_enums.EvaluatorStates.LONG\n        )\n        quantity = decimal.Decimal(5)\n        orders = await binance_consumer._create_secondary_arbitrage_order(arbitrage, quantity)\n        assert orders\n\n        limit_order = orders[0]\n        assert limit_order.exchange_order_type is trading_enums.TradeOrderType.LIMIT\n        assert limit_order.order_type is trading_enums.TraderOrderType.SELL_LIMIT\n        assert limit_order.side is trading_enums.TradeOrderSide.SELL\n        assert limit_order.symbol == binance_consumer.trading_mode.symbol\n        assert limit_order.order_id == arbitrage.secondary_limit_order_id\n        assert limit_order.origin_quantity == quantity\n        assert limit_order.associated_entry_ids is None\n        assert limit_order.is_active is True\n\n        order_group_1 = limit_order.order_group\n        stop_order = order_group_1.get_group_open_orders()[1]\n        assert order_group_1 is stop_order.order_group\n        assert stop_order.exchange_order_type is trading_enums.TradeOrderType.STOP_LOSS\n        assert stop_order.order_type is trading_enums.TraderOrderType.STOP_LOSS\n        assert stop_order.side is trading_enums.TradeOrderSide.SELL\n        assert stop_order.symbol == binance_consumer.trading_mode.symbol\n        assert stop_order.order_id == arbitrage.secondary_stop_order_id\n        assert stop_order.origin_quantity == quantity\n        assert stop_order.is_active is True\n        assert limit_order.associated_entry_ids is None\n\n        # enable inactive orders\n        exchange_manager.trader.enable_inactive_orders = True\n\n        # short\n        arbitrage = arbitrage_container_import.ArbitrageContainer(\n            price, decimal.Decimal(15), trading_enums.EvaluatorStates.SHORT\n        )\n        arbitrage.initial_limit_order_id = \"123\"\n        quantity = decimal.Decimal(5)\n        orders = await binance_consumer._create_secondary_arbitrage_order(arbitrage, quantity)\n        assert orders\n\n        limit_order = orders[0]\n        assert limit_order.exchange_order_type is trading_enums.TradeOrderType.LIMIT\n        assert limit_order.order_type is trading_enums.TraderOrderType.BUY_LIMIT\n        assert limit_order.side is trading_enums.TradeOrderSide.BUY\n        assert limit_order.symbol == binance_consumer.trading_mode.symbol\n        assert limit_order.order_id == arbitrage.secondary_limit_order_id\n        assert limit_order.origin_quantity == quantity\n        assert limit_order.is_active is False\n        assert limit_order.associated_entry_ids == [\"123\"]\n\n        order_group_2 = limit_order.order_group\n        stop_order = order_group_2.get_group_open_orders()[1]\n        assert order_group_2 is stop_order.order_group\n        assert order_group_2 != order_group_1\n        assert stop_order.exchange_order_type is trading_enums.TradeOrderType.STOP_LOSS\n        assert stop_order.order_type is trading_enums.TraderOrderType.STOP_LOSS\n        assert stop_order.side is trading_enums.TradeOrderSide.BUY\n        assert stop_order.symbol == binance_consumer.trading_mode.symbol\n        assert stop_order.order_id == arbitrage.secondary_stop_order_id\n        assert stop_order.origin_quantity == quantity\n        assert stop_order.is_active is True\n        assert limit_order.associated_entry_ids == [\"123\"]\n\n\nasync def test_get_quantity_from_holdings():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as arbitrage_trading_mode_tests.exchange_tuple:\n        _, binance_consumer, _ = arbitrage_trading_mode_tests.exchange_tuple\n        binance_consumer.PORTFOLIO_PERCENT_PER_TRADE = decimal.Decimal(str(0.5))\n        assert binance_consumer._get_quantity_from_holdings(decimal.Decimal(str(10)), decimal.Decimal(str(100)), trading_enums.EvaluatorStates.SHORT) == decimal.Decimal(str(5))\n        assert binance_consumer._get_quantity_from_holdings(decimal.Decimal(str(10)), decimal.Decimal(str(100)), trading_enums.EvaluatorStates.LONG) == decimal.Decimal(str(50))\n\n\nasync def test_get_stop_loss_price():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as arbitrage_trading_mode_tests.exchange_tuple:\n        _, binance_consumer, arbitrage_trading_mode_tests.exchange_manager = arbitrage_trading_mode_tests.exchange_tuple\n        binance_consumer.STOP_LOSS_DELTA_FROM_OWN_PRICE = decimal.Decimal(str(0.01))\n        symbol_market = arbitrage_trading_mode_tests.exchange_manager.exchange.get_market_status(\"BTC/USDT\",\n                                                                                                 with_fixer=False)\n        assert binance_consumer._get_stop_loss_price(symbol_market, decimal.Decimal(str(100)), True) == decimal.Decimal(str(99))\n        assert binance_consumer._get_stop_loss_price(symbol_market, decimal.Decimal(str(100)), False) == decimal.Decimal(str(101))\n"
  },
  {
    "path": "Trading/Mode/arbitrage_trading_mode/tests/test_arbitrage_trading_mode_producer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport mock\nimport decimal\n\nimport octobot_commons.pretty_printer as pretty_printer\nimport octobot_trading.enums as trading_enums\nimport tentacles.Trading.Mode.arbitrage_trading_mode.arbitrage_container as arbitrage_container_import\nimport tentacles.Trading.Mode.arbitrage_trading_mode.tests as arbitrage_trading_mode_tests\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_init():\n    tentacles_manager_api.reload_tentacle_info()\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n\n        # producer\n        assert binance_producer.own_exchange_mark_price is None\n        assert binance_producer.other_exchanges_mark_prices == {}\n        assert binance_producer.sup_triggering_price_delta_ratio > 1\n        assert binance_producer.inf_triggering_price_delta_ratio < 1\n        assert binance_producer.base\n        assert binance_producer.quote\n        assert binance_producer.lock\n\n\nasync def test_own_exchange_mark_price_callback():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, _, _ = exchange_tuple\n\n        with mock.patch.object(binance_producer, \"_create_arbitrage_initial_order\", new=mock.AsyncMock()) as order_mock:\n            # no other exchange mark price yet\n            await binance_producer._own_exchange_mark_price_callback(\"\", \"\", \"\", \"\", 11)\n            assert binance_producer.own_exchange_mark_price == decimal.Decimal(11)\n            order_mock.assert_not_called()\n\n            binance_producer.other_exchanges_mark_prices[\"kraken\"] = decimal.Decimal(20)\n            binance_producer.other_exchanges_mark_prices[\"bitfinex\"] = decimal.Decimal(22)\n            # other exchange mark price is set\n            await binance_producer._own_exchange_mark_price_callback(\"\", \"\", \"\", \"\", 11)\n            order_mock.assert_called_once()\n\n\nasync def test_mark_price_callback():\n    binance = \"binance\"\n    kraken = \"kraken\"\n    async with arbitrage_trading_mode_tests.exchange(binance) as binance_tuple, \\\n            arbitrage_trading_mode_tests.exchange(kraken, backtesting=binance_tuple[2].backtesting) as kraken_tuple:\n        binance_producer, _, _ = binance_tuple\n        kraken_producer, _, _ = kraken_tuple\n\n        with mock.patch.object(binance_producer, \"_create_arbitrage_initial_order\",\n                               new=mock.AsyncMock()) as binance_order_mock, \\\n                mock.patch.object(kraken_producer, \"_create_arbitrage_initial_order\",\n                                  new=mock.AsyncMock()) as kraken_order_mock:\n            # no own exchange price yet\n            await kraken_producer._mark_price_callback(binance, \"\", \"\", \"\", 1000)\n            kraken_order_mock.assert_not_called()\n            await binance_producer._mark_price_callback(kraken, \"\", \"\", \"\", 1000)\n            binance_order_mock.assert_not_called()\n\n            # set own exchange mark price on kraken\n            kraken_producer.own_exchange_mark_price = decimal.Decimal(900)\n            # no effect on binance\n            await binance_producer._mark_price_callback(kraken, \"\", \"\", \"\", 1000)\n            binance_order_mock.assert_not_called()\n            # create arbitrage on kraken\n            await kraken_producer._mark_price_callback(binance, \"\", \"\", \"\", 1000)\n            kraken_order_mock.assert_called_once()\n\n\nasync def test_order_filled_callback():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n        order_id = \"1\"\n        price = 10\n        quantity = 3\n        fees = 0.1\n        fees_currency = \"BTC\"\n        symbol = \"BTC/USD\"\n        order_dict = get_order_dict(order_id, symbol, price, quantity, trading_enums.OrderStatus.FILLED.value,\n                                    trading_enums.TradeOrderType.LIMIT.value, fees, fees_currency)\n        with mock.patch.object(binance_producer, \"_close_arbitrage\", new=mock.Mock()) as close_mock, \\\n                mock.patch.object(binance_producer, \"_trigger_arbitrage_secondary_order\",\n                                  new=mock.AsyncMock()) as trigger_mock, \\\n                mock.patch.object(binance_producer, \"_log_results\", new=mock.Mock()) as result_mock:\n            # nothing happens: order id not in open arbitrages\n            await binance_producer.order_filled_callback(order_dict)\n            close_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n\n            # order id now in open arbitrages\n            arbitrage = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(price)), decimal.Decimal(15), trading_enums.EvaluatorStates.LONG)\n            arbitrage.initial_limit_order_id = order_id\n            binance_consumer.open_arbitrages.append(arbitrage)\n\n            await binance_producer.order_filled_callback(order_dict)\n            close_mock.assert_not_called()\n            result_mock.assert_not_called()\n            # call create secondary order\n            trigger_mock.assert_called_once()\n            trigger_mock.reset_mock()\n\n            # last step case 1: close arbitrage: fill callback with secondary limit order\n            limit_id = \"2\"\n            arbitrage.passed_initial_order = True\n            arbitrage.secondary_limit_order_id = limit_id\n            arbitrage.initial_before_fee_filled_quantity = decimal.Decimal(str(29.9))\n            sec_limit_order_dict = get_order_dict(limit_id, symbol, price, quantity,\n                                                  trading_enums.OrderStatus.FILLED.value,\n                                                  trading_enums.TradeOrderType.LIMIT.value, fees, fees_currency)\n            await binance_producer.order_filled_callback(sec_limit_order_dict)\n            # call close arbitrage\n            close_mock.assert_called_once()\n            trigger_mock.assert_not_called()\n            result_mock.assert_called_once()\n            _, arbitrage_success, filled_quantity = result_mock.mock_calls[0].args\n            assert arbitrage_success\n            assert filled_quantity == quantity * price\n            close_mock.reset_mock()\n            result_mock.reset_mock()\n\n            # last step case 2: close arbitrage: fill callback with secondary stop order\n            stop_id = \"3\"\n            arbitrage.secondary_stop_order_id = stop_id\n            sec_stop_order_dict = get_order_dict(stop_id, symbol, price, quantity,\n                                                 trading_enums.OrderStatus.FILLED.value,\n                                                 trading_enums.TradeOrderType.STOP_LOSS.value, fees, fees_currency)\n            await binance_producer.order_filled_callback(sec_stop_order_dict)\n            # call close arbitrage\n            close_mock.assert_called_once()\n            result_mock.assert_called_once()\n            _, arbitrage_success, filled_quantity = result_mock.mock_calls[0].args\n            assert not arbitrage_success\n            assert filled_quantity == quantity * price\n            trigger_mock.assert_not_called()\n\n\nasync def test_order_cancelled_callback():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n        order_id = \"1\"\n        price = 10\n        quantity = 3\n        fees = 0.1\n        fees_currency = \"BTC\"\n        symbol = \"BTC/USD\"\n        order_dict = get_order_dict(order_id, symbol, price, quantity, trading_enums.OrderStatus.FILLED.value,\n                                    trading_enums.TradeOrderType.LIMIT.value, fees, fees_currency)\n        with mock.patch.object(binance_producer, \"_close_arbitrage\", new=mock.Mock()) as close_mock:\n            # no open arbitrage\n            await binance_producer.order_cancelled_callback(order_dict)\n            close_mock.assert_not_called()\n\n            # open arbitrage with different order id: nothing happens\n            arbitrage = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(price)), decimal.Decimal(15), trading_enums.EvaluatorStates.LONG)\n            binance_consumer.open_arbitrages.append(arbitrage)\n            await binance_producer.order_cancelled_callback(order_dict)\n            close_mock.assert_not_called()\n\n            # open arbitrage with this order id: arbitrage gets closed\n            arbitrage.initial_limit_order_id = order_id\n            await binance_producer.order_cancelled_callback(order_dict)\n            close_mock.assert_called_once()\n\n\nasync def test_analyse_arbitrage_opportunities():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, _, _ = exchange_tuple\n\n        with mock.patch.object(binance_producer, \"_ensure_no_expired_opportunities\",\n                               new=mock.AsyncMock()) as expiration_mock, \\\n                mock.patch.object(binance_producer, \"_trigger_arbitrage_opportunity\",\n                                  new=mock.AsyncMock()) as trigger_mock:\n            # long opportunity 1\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(10))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(100)), \"binanceje\": decimal.Decimal(str(200)), \"bitfinex\": decimal.Decimal(str(150))}\n            # long enabled\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_called_once_with(decimal.Decimal(str(150)), trading_enums.EvaluatorStates.LONG)\n            trigger_mock.assert_called_once_with(decimal.Decimal(str(150)), trading_enums.EvaluatorStates.LONG)\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n            # long disabled\n            binance_producer.enable_longs = False\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n\n            # short opportunity 1\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(100))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(70)), \"binanceje\": decimal.Decimal(str(71)), \"bitfinex\": decimal.Decimal(str(75))}\n            # short enabled\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_called_once_with(decimal.Decimal(str(72)), trading_enums.EvaluatorStates.SHORT)\n            trigger_mock.assert_called_once_with(decimal.Decimal(str(72)), trading_enums.EvaluatorStates.SHORT)\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n            # short disabled\n            binance_producer.enable_shorts = False\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n\n            binance_producer.enable_longs = True\n            binance_producer.enable_shorts = True\n\n            # long opportunity but price too close to current price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(71.99))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(70)), \"binanceje\": decimal.Decimal(str(71)), \"bitfinex\": decimal.Decimal(str(75))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n\n            # short opportunity but price too close to current price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(72.01))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(70)), \"binanceje\": decimal.Decimal(str(71)), \"bitfinex\": decimal.Decimal(str(75))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n\n            # with higher numbers\n            # higher numbers long opportunity\n            # max long exclusive trigger should be 9803.921568627451 on own_exchange_mark_price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(9802.9999))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(9000)), \"binanceje\": decimal.Decimal(str(10000)), \"bitfinex\": decimal.Decimal(str(11000))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_called_once_with(decimal.Decimal(str(10000)), trading_enums.EvaluatorStates.LONG)\n            trigger_mock.assert_called_once_with(decimal.Decimal(str(10000)), trading_enums.EvaluatorStates.LONG)\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n\n            # higher numbers long opportunity: fail to pass threshold 1\n            # max long exclusive trigger should be 9803.921568627451 on own_exchange_mark_price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(9803.921568627451))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(9000)), \"binanceje\": decimal.Decimal(str(10000)), \"bitfinex\": decimal.Decimal(str(11000))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n\n            # higher numbers long opportunity: fail to pass threshold 2\n            # max long exclusive trigger should be 9803.921568627451 on own_exchange_mark_price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(9803.9216))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(9000)), \"binanceje\": decimal.Decimal(str(10000)), \"bitfinex\": decimal.Decimal(str(11000))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n\n            # higher numbers short opportunity\n            # min short exclusive trigger should be 10204.081632653062 on own_exchange_mark_price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(10205))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(9000)), \"binanceje\": decimal.Decimal(str(10000)), \"bitfinex\": decimal.Decimal(str(11000))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_called_once_with(decimal.Decimal(str(10000)), trading_enums.EvaluatorStates.SHORT)\n            trigger_mock.assert_called_once_with(decimal.Decimal(str(10000)), trading_enums.EvaluatorStates.SHORT)\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n\n            # higher numbers short opportunity: fail to pass threshold 1\n            # min short exclusive trigger should be 10204.081632653062 on own_exchange_mark_price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(10203.081632653062))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(9000)), \"binanceje\": decimal.Decimal(str(10000)), \"bitfinex\": decimal.Decimal(str(11000))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n\n            # higher numbers short opportunity: fail to pass threshold 2\n            # min short exclusive trigger should be 10204.081632653062 on own_exchange_mark_price\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(10204.0815))\n            binance_producer.other_exchanges_mark_prices = {\"kraken\": decimal.Decimal(str(9000)), \"binanceje\": decimal.Decimal(str(10000)), \"bitfinex\": decimal.Decimal(str(11000))}\n            await binance_producer._analyse_arbitrage_opportunities()\n            expiration_mock.assert_not_called()\n            trigger_mock.assert_not_called()\n            expiration_mock.reset_mock()\n            trigger_mock.reset_mock()\n\n\nasync def test_trigger_arbitrage_opportunity():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, _, _ = exchange_tuple\n\n        with mock.patch.object(binance_producer, \"_create_arbitrage_initial_order\", new=mock.AsyncMock()) as order_mock, \\\n                mock.patch.object(binance_producer, \"_register_state\", new=mock.Mock()) as register_mock, \\\n                mock.patch.object(binance_producer, \"_log_arbitrage_opportunity_details\", new=mock.Mock()) as \\\n                log_arbitrage_opportunity_details_mock:\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(10))\n            await binance_producer._trigger_arbitrage_opportunity(15, trading_enums.EvaluatorStates.LONG)\n            order_mock.assert_called_once()\n            register_mock.assert_called_once_with(trading_enums.EvaluatorStates.LONG, decimal.Decimal(str(5)))\n            log_arbitrage_opportunity_details_mock.assert_called_once_with(decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n\n\nasync def test_log_arbitrage_opportunity_details():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, _, _ = exchange_tuple\n        binance_producer.own_exchange_mark_price = decimal.Decimal(str(100))\n        debug_mock = mock.Mock()\n        # do not mock with context manager to keep the mock in teardown\n        binance_producer.logger = debug_mock\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(99.999)),  trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(-0.001)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(90)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(-10)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(1)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(-99)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(0)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(-100)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(0)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(0)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(100.00001)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(0.00001)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(110)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(10)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(150)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(50)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(250)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(150)}%\" in debug_mock.debug.call_args[0][0]\n\n        binance_producer._log_arbitrage_opportunity_details(decimal.Decimal(str(20100)), trading_enums.EvaluatorStates.LONG)\n        assert f\"{pretty_printer.round_with_decimal_count(20000)}%\" in debug_mock.debug.call_args[0][0]\n\n\nasync def test_trigger_arbitrage_secondary_order():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, _, _ = exchange_tuple\n        order_id = \"1\"\n        price = 10\n        quantity = 3\n        fees = 0.1\n        fees_currency = \"BTC\"\n        symbol = \"BTC/USDT\"\n        order_dict = get_order_dict(order_id, symbol, price, quantity, trading_enums.OrderStatus.FILLED.value,\n                                    trading_enums.TradeOrderType.LIMIT.value, fees, fees_currency)\n        with mock.patch.object(binance_producer, \"_create_arbitrage_secondary_order\",\n                               new=mock.AsyncMock()) as order_mock:\n            # long: already bought, is now selling\n            arbitrage = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(price)), decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n            await binance_producer._trigger_arbitrage_secondary_order(arbitrage, order_dict, 3)\n            updated_arbitrage, secondary_quantity = order_mock.mock_calls[0].args\n            assert updated_arbitrage is arbitrage\n            assert arbitrage.passed_initial_order\n            assert arbitrage.initial_before_fee_filled_quantity == decimal.Decimal(str(30))\n            assert secondary_quantity == decimal.Decimal(str(2.9))\n            order_mock.reset_mock()\n\n            # short: already sold, is now buying: no fee on base side\n            arbitrage_2 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(price)), decimal.Decimal(str(7)), trading_enums.EvaluatorStates.SHORT)\n            await binance_producer._trigger_arbitrage_secondary_order(arbitrage_2, order_dict, 3)\n            updated_arbitrage, secondary_quantity = order_mock.mock_calls[0].args\n            assert updated_arbitrage is arbitrage_2\n            assert arbitrage_2.passed_initial_order\n            assert arbitrage_2.initial_before_fee_filled_quantity == decimal.Decimal(str(3))\n            assert round(secondary_quantity, 5) == decimal.Decimal(\"4.14286\")\n            order_mock.reset_mock()\n\n            # short: already sold, is now buying: fee on base side\n            arbitrage_3 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(price)), decimal.Decimal(str(7)), trading_enums.EvaluatorStates.SHORT)\n            order_dict = get_order_dict(order_id, symbol, price, quantity, trading_enums.OrderStatus.FILLED.value,\n                                        trading_enums.TradeOrderType.STOP_LOSS.value, fees, \"USDT\")\n            await binance_producer._trigger_arbitrage_secondary_order(arbitrage_3, order_dict, 3)\n            updated_arbitrage, secondary_quantity = order_mock.mock_calls[0].args\n            assert updated_arbitrage is arbitrage_3\n            assert arbitrage_3.passed_initial_order\n            assert arbitrage_3.initial_before_fee_filled_quantity == decimal.Decimal(str(3))\n            assert round(secondary_quantity, 5) == decimal.Decimal(\"4.27143\")\n\n\nasync def test_ensure_no_existing_arbitrage_on_this_price():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n        arbitrage_1 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(10)), decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n        arbitrage_2 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(20)), decimal.Decimal(str(18)), trading_enums.EvaluatorStates.SHORT)\n        binance_consumer.open_arbitrages = [arbitrage_1, arbitrage_2]\n\n        binance_producer.own_exchange_mark_price = 9\n        assert binance_producer._ensure_no_existing_arbitrage_on_this_price(trading_enums.EvaluatorStates.LONG)\n        assert binance_producer._ensure_no_existing_arbitrage_on_this_price(trading_enums.EvaluatorStates.SHORT)\n\n        for price in (9.99, 10, 11, 15):\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(price))\n            assert not binance_producer._ensure_no_existing_arbitrage_on_this_price(trading_enums.EvaluatorStates.LONG)\n            assert binance_producer._ensure_no_existing_arbitrage_on_this_price(trading_enums.EvaluatorStates.SHORT)\n\n        for price in (18, 17.99, 20, 20.001):\n            binance_producer.own_exchange_mark_price = decimal.Decimal(str(price))\n            assert binance_producer._ensure_no_existing_arbitrage_on_this_price(trading_enums.EvaluatorStates.LONG)\n            assert not binance_producer._ensure_no_existing_arbitrage_on_this_price(trading_enums.EvaluatorStates.SHORT)\n\n\nasync def test_get_arbitrage():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n        arbitrage_1 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(10)), decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n        arbitrage_2 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(20)), decimal.Decimal(str(18)), trading_enums.EvaluatorStates.SHORT)\n        binance_consumer.open_arbitrages = [arbitrage_1, arbitrage_2]\n        arbitrage_1.initial_limit_order_id = \"1\"\n        assert arbitrage_1 is binance_producer._get_arbitrage(\"1\")\n        assert None is binance_producer._get_arbitrage(\"2\")\n\n\nasync def test_ensure_no_expired_opportunities():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, exchange_manager = exchange_tuple\n        arbitrage_1 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(10)), decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n        arbitrage_2 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(20)), decimal.Decimal(str(17)), trading_enums.EvaluatorStates.SHORT)\n        binance_consumer.open_arbitrages = [arbitrage_1, arbitrage_2]\n\n        with mock.patch.object(binance_producer, \"_cancel_order\", new=mock.AsyncMock()) as cancel_order_mock:\n            # average price is 18\n            # long order is valid\n            # short order is expired (price > 17)\n            await binance_producer._ensure_no_expired_opportunities(decimal.Decimal(str(18)), trading_enums.EvaluatorStates.LONG)\n            assert arbitrage_2 not in binance_consumer.open_arbitrages\n            cancel_order_mock.assert_called_once()\n            cancel_order_mock.reset_mock()\n\n            await binance_producer._ensure_no_expired_opportunities(decimal.Decimal(str(18)), trading_enums.EvaluatorStates.SHORT)\n            assert binance_consumer.open_arbitrages == [arbitrage_1]\n            cancel_order_mock.assert_not_called()\n\n\nasync def test_close_arbitrage():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n        arbitrage_1 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(10)), decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n        arbitrage_2 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(20)), decimal.Decimal(str(17)), trading_enums.EvaluatorStates.SHORT)\n        binance_consumer.open_arbitrages = [arbitrage_1, arbitrage_2]\n        binance_producer._close_arbitrage(arbitrage_1)\n        assert arbitrage_1 not in binance_consumer.open_arbitrages\n        assert binance_producer.state is trading_enums.EvaluatorStates.NEUTRAL\n        assert binance_producer.final_eval == \"\"\n\n\nasync def test_get_open_arbitrages():\n    binance = \"binance\"\n    kraken = \"kraken\"\n    async with arbitrage_trading_mode_tests.exchange(binance) as binance_tuple, \\\n            arbitrage_trading_mode_tests.exchange(kraken, backtesting=binance_tuple[2].backtesting) as kraken_tuple:\n        binance_producer, binance_consumer, _ = binance_tuple\n        kraken_producer, kraken_consumer, _ = kraken_tuple\n        arbitrage_1 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(10)), decimal.Decimal(str(15)), trading_enums.EvaluatorStates.LONG)\n        arbitrage_2 = arbitrage_container_import.ArbitrageContainer(decimal.Decimal(str(20)), decimal.Decimal(str(17)), trading_enums.EvaluatorStates.SHORT)\n        binance_consumer.open_arbitrages = [arbitrage_1, arbitrage_2]\n        assert kraken_consumer.open_arbitrages == []\n        assert binance_producer._get_open_arbitrages() is binance_consumer.open_arbitrages\n        assert kraken_producer._get_open_arbitrages() is kraken_consumer.open_arbitrages\n\n\nasync def test_register_state():\n    async with arbitrage_trading_mode_tests.exchange(\"binance\") as exchange_tuple:\n        binance_producer, binance_consumer, _ = exchange_tuple\n        assert binance_producer.state is trading_enums.EvaluatorStates.NEUTRAL\n        binance_producer._register_state(trading_enums.EvaluatorStates.LONG, decimal.Decimal(str(1)))\n        assert binance_producer.state is trading_enums.EvaluatorStates.LONG\n        assert \"1\" in binance_producer.final_eval\n\n\ndef get_order_dict(order_id, symbol, price, quantity, status, order_type, fees_amount, fees_currency):\n    return {\n        trading_enums.ExchangeConstantsOrderColumns.ID.value: order_id,\n        trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value: symbol,\n        trading_enums.ExchangeConstantsOrderColumns.PRICE.value: price,\n        trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value: quantity,\n        trading_enums.ExchangeConstantsOrderColumns.FILLED.value: quantity,\n        trading_enums.ExchangeConstantsOrderColumns.STATUS.value: status,\n        trading_enums.ExchangeConstantsOrderColumns.TYPE.value: order_type,\n        trading_enums.ExchangeConstantsOrderColumns.FEE.value: {\n            trading_enums.FeePropertyColumns.CURRENCY.value: fees_currency,\n            trading_enums.FeePropertyColumns.COST.value: fees_amount,\n            trading_enums.FeePropertyColumns.IS_FROM_EXCHANGE.value: True\n        },\n    }\n"
  },
  {
    "path": "Trading/Mode/blank_trading_mode/__init__.py",
    "content": "from .blank_trading import BlankTradingMode\n"
  },
  {
    "path": "Trading/Mode/blank_trading_mode/blank_trading.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n\nimport octobot_trading.enums as enums\nimport octobot_trading.modes as trading_modes\n\n\nclass BlankTradingMode(trading_modes.AbstractTradingMode):\n    \"\"\"\n    This trading mode is doing nothing. It is to be selected when no trading is to be done.\n    \"\"\"\n\n    @staticmethod\n    def is_backtestable():\n        return False\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            enums.ExchangeTypes.SPOT,\n            enums.ExchangeTypes.FUTURE\n        ]\n"
  },
  {
    "path": "Trading/Mode/blank_trading_mode/config/BlankTradingMode.json",
    "content": "{\n    \"default_config\": [\n        \"BlankStrategyEvaluator\"\n    ],\n    \"required_strategies\": [\n        \"BlankStrategyEvaluator\"\n    ]\n}"
  },
  {
    "path": "Trading/Mode/blank_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"BlankTradingMode\"],\n  \"tentacles-requirements\": [\"blank_strategy\"]\n}"
  },
  {
    "path": "Trading/Mode/blank_trading_mode/resources/BlankTradingMode.md",
    "content": "Blank trading mode is not trading. \n\nActivate it to disable new order creation on your OctoBot.\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/__init__.py",
    "content": "from .daily_trading import DailyTradingMode"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/config/DailyTradingMode.json",
    "content": "{\n    \"default_config\": [\n        \"SimpleStrategyEvaluator\"\n    ],\n    \"required_strategies\": [\n        \"SimpleStrategyEvaluator\",\n        \"TechnicalAnalysisStrategyEvaluator\"\n    ],\n    \"required_strategies_min_count\": 1,\n    \"target_profits_mode\": false,\n    \"use_prices_close_to_current_price\": false,\n    \"close_to_current_price_difference\": 0.005,\n    \"target_profits_mode_take_profit\": 5,\n    \"target_profits_mode_enable_position_increase\": false,\n    \"use_stop_orders\": true,\n    \"target_profits_mode_stop_loss\": 2.5,\n    \"buy_with_maximum_size_orders\": false,\n    \"sell_with_maximum_size_orders\": false,\n    \"buy_order_amount\": \"\",\n    \"sell_order_amount\": \"\",\n    \"disable_sell_orders\": false,\n    \"disable_buy_orders\": false,\n    \"max_currency_percent\": 100,\n    \"emit_trading_signals\": false\n}"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/daily_trading.pxd",
    "content": "# cython: language_level=3\n#  Drakkar-Software OctoBot-Trading\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nfrom octobot_trading.producers.abstract_mode_producer cimport AbstractTradingModeProducer\n\nfrom octobot_trading.consumers.abstract_mode_consumer cimport AbstractTradingModeConsumer\n\nfrom octobot_trading.modes.abstract_trading_mode cimport AbstractTradingMode\n\n\ncdef class DailyTradingMode(AbstractTradingMode):\n    pass\n\n\ncdef class DailyTradingModeConsumer(AbstractTradingModeConsumer):\n    cdef public double MAX_SUM_RESULT\n\n    cdef public double STOP_LOSS_ORDER_MAX_PERCENT\n    cdef public double STOP_LOSS_ORDER_MIN_PERCENT\n    cdef public double STOP_LOSS_ORDER_ATTENUATION\n\n    cdef public double QUANTITY_MIN_PERCENT\n    cdef public double QUANTITY_MAX_PERCENT\n    cdef public double QUANTITY_ATTENUATION\n\n    cdef public double QUANTITY_MARKET_MIN_PERCENT\n    cdef public double QUANTITY_MARKET_MAX_PERCENT\n    cdef public double QUANTITY_BUY_MARKET_ATTENUATION\n    cdef public double QUANTITY_MARKET_ATTENUATION\n\n    cdef public double BUY_LIMIT_ORDER_MAX_PERCENT\n    cdef public double BUY_LIMIT_ORDER_MIN_PERCENT\n    cdef public double SELL_LIMIT_ORDER_MIN_PERCENT\n    cdef public double SELL_LIMIT_ORDER_MAX_PERCENT\n    cdef public double LIMIT_ORDER_ATTENUATION\n\n    cdef public double QUANTITY_RISK_WEIGHT\n    cdef public double MAX_QUANTITY_RATIO\n    cdef public double MIN_QUANTITY_RATIO\n    cdef public double DELTA_RATIO\n\n    cdef public double SELL_MULTIPLIER\n    cdef public double FULL_SELL_MIN_RATIO\n\n    cdef public bint USE_CLOSE_TO_CURRENT_PRICE\n    cdef public double CLOSE_TO_CURRENT_PRICE_DEFAULT_RATIO\n    cdef public bint BUY_WITH_MAXIMUM_SIZE_ORDERS\n    cdef public bint SELL_WITH_MAXIMUM_SIZE_ORDERS\n    cdef public bint DISABLE_BUY_ORDERS\n    cdef public bint DISABLE_SELL_ORDERS\n    cdef public bint USE_STOP_ORDERS\n\n    cpdef __get_limit_price_from_risk(self, object eval_note)\n    cpdef __get_stop_price_from_risk(self)\n    cpdef __get_buy_limit_quantity_from_risk(self, object eval_note, double quantity, str quote)\n    cpdef __get_market_quantity_from_risk(self, object eval_note, double quantity, str quote, bint selling=*)\n\ncdef class DailyTradingModeProducer(AbstractTradingModeProducer):\n    cdef public object state\n\n    cdef public double VERY_LONG_THRESHOLD\n    cdef public double LONG_THRESHOLD\n    cdef public double NEUTRAL_THRESHOLD\n    cdef public double SHORT_THRESHOLD\n    cdef public double RISK_THRESHOLD\n\n    cpdef double __get_delta_risk(self)\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/daily_trading.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport decimal\nimport math\nimport dataclasses\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.evaluators_util as evaluators_util\nimport octobot_commons.pretty_printer as pretty_printer\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.signals as signals\nimport octobot_evaluators.api as evaluators_api\nimport octobot_evaluators.constants as evaluators_constants\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_evaluators.matrix as matrix\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.api as trading_api\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.modes.script_keywords as script_keywords\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.personal_data as trading_personal_data\n\n\n@dataclasses.dataclass\nclass OrderDetails:\n    price: decimal.Decimal\n    quantity: typing.Optional[decimal.Decimal]\n\n\nclass DailyTradingMode(trading_modes.AbstractTradingMode):\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.UI.user_input(\n            \"target_profits_mode\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"Target profits mode: Enable target profits mode. In this mode, only entry \"\n                  \"signals are taken into account (usually LONG signals). When an entry is filled, \"\n                  \"a take profit will instantly be created using the '[Target profits mode] Take profit' setting. \"\n                  \"A stop loss can also be created using the '[Target profits mode] Stop loss' setting if \"\n                  \"'Stop orders' are enabled.\",\n        )\n\n        self.UI.user_input(\n            \"use_prices_close_to_current_price\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"Fixed limit prices: Use a fixed ratio to compute prices in sell / buy orders.\",\n        )\n        self.UI.user_input(\n            \"close_to_current_price_difference\", commons_enums.UserInputTypes.FLOAT, 0.005, inputs,\n            min_val=0,\n            title=\"Fixed limit prices difference: Multiplier to take into account when placing a limit order \"\n                  \"(used if fixed limit prices is enabled). For a 200 USD price and 0.005 in difference: \"\n                  \"buy price would be 199 and sell price 201.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"use_prices_close_to_current_price\": True\n                }\n            }\n        )\n        self.UI.user_input(\n            \"target_profits_mode_take_profit\", commons_enums.UserInputTypes.FLOAT, 5, inputs,\n            min_val=0,\n            title=\"[Target profits mode] Take profit: percent profits to compute the take profit order price from. \"\n                  \"Only used in 'Target profits mode'. \"\n                  \"Example: a buy entry at 300 with a 'Take profit' at 10 will create a sell order at 330.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"target_profits_mode\": True\n                }\n            }\n        )\n        self.UI.user_input(\n            \"use_stop_orders\", commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=\"Stop orders: Create a stop loss alongside sell orders.\",\n        )\n        self.UI.user_input(\n            \"target_profits_mode_stop_loss\", commons_enums.UserInputTypes.FLOAT, 2.5, inputs,\n            min_val=0, max_val=100,\n            title=\"[Target profits mode] Stop loss: maximum percent losses to compute the stop loss price from. \"\n                  \"Only used in 'Target profits mode'. \"\n                  \"Example: a buy entry at 300 with a 'Stop loss' at 10 will create a stop order at 270.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"target_profits_mode\": True,\n                  \"use_stop_orders\": True,\n                }\n            }\n        )\n        self.UI.user_input(\n            \"target_profits_mode_enable_position_increase\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"[Target profits mode] Enable futures position increase: Allow to increase a previously open \"\n                  \"position when receiving a new signal. \"\n                  \"Only used in 'Target profits mode' when trading futures. \"\n                  \"Example: increase a $100 LONG position to $150 by adding $50 more when a new LONG signal is \"\n                  \"received. WARNING: enabling this option can lead to liquidation price changes as positions \"\n                  \"build up and end up liquidating a position before initial stop loss prices are reached.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"target_profits_mode\": True\n                }\n            }\n        )\n        self.UI.user_input(\n            \"buy_with_maximum_size_orders\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"All in buy trades: Trade with all available funds at each buy order.\",\n        )\n        self.UI.user_input(\n            \"sell_with_maximum_size_orders\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"All in sell trades: Trade with all available funds at each sell order.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"target_profits_mode\": False\n                }\n            }\n        )\n        trading_modes.user_select_order_amount(\n            self, inputs,\n            buy_dependencies={\"buy_with_maximum_size_orders\": False},\n            sell_dependencies={\"target_profits_mode\": False, \"sell_with_maximum_size_orders\": False}\n        )\n        self.UI.user_input(\n            \"disable_sell_orders\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"Disable sell orders (sell market and sell limit).\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"target_profits_mode\": False\n                }\n            }\n        )\n        self.UI.user_input(\n            \"disable_buy_orders\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"Disable buy orders (buy market and buy limit).\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  \"target_profits_mode\": False\n                }\n            }\n        )\n        self.UI.user_input(\n            \"max_currency_percent\", commons_enums.UserInputTypes.FLOAT, 100, inputs,\n            min_val=0, max_val=100,\n            title=\"Maximum currency percent: Maximum portfolio % to allocate on a given currency. \"\n                  \"Used to compute buy order amounts. Ignored when 'Amount per buy/entry order' is set.\",\n        )\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_current_state(self) -> (str, float):\n        return super().get_current_state()[0] if self.producers[0].state is None else self.producers[0].state.name, \\\n               self.producers[0].final_eval\n\n    def get_mode_producer_classes(self) -> list:\n        return [DailyTradingModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [DailyTradingModeConsumer]\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n\nclass DailyTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    PRICE_KEY = \"PRICE\"\n    VOLUME_KEY = \"VOLUME\"\n    STOP_PRICE_KEY = \"STOP_PRICE\"\n    ACTIVE_ORDER_SWAP_STRATEGY = \"ACTIVE_ORDER_SWAP_STRATEGY\"\n    ACTIVE_ORDER_SWAP_TIMEOUT = \"ACTIVE_ORDER_SWAP_TIMEOUT\"\n    TAKE_PROFIT_PRICE_KEY = \"TAKE_PROFIT_PRICE\"\n    ADDITIONAL_TAKE_PROFIT_PRICES_KEY = \"ADDITIONAL_TAKE_PROFIT_PRICES\"\n    ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY = \"ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS\"\n    STOP_ONLY = \"STOP_ONLY\"\n    TRAILING_PROFILE = \"TRAILING_PROFILE\"\n    CANCEL_POLICY = \"CANCEL_POLICY\"\n    CANCEL_POLICY_PARAMS = \"CANCEL_POLICY_PARAMS\"\n    REDUCE_ONLY_KEY = \"REDUCE_ONLY\"\n    TAG_KEY = \"TAG\"\n    EXCHANGE_ORDER_IDS = \"EXCHANGE_ORDER_IDS\"\n    LEVERAGE = \"LEVERAGE\"\n    ORDER_EXCHANGE_CREATION_PARAMS = \"ORDER_EXCHANGE_CREATION_PARAMS\"\n    TARGET_PROFIT_MODE_ENTRY_QUANTITY_SIDE = trading_enums.TradeOrderSide.BUY\n\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self.trader = self.exchange_manager.trader\n\n        self.MAX_SUM_RESULT = decimal.Decimal(2)\n\n        self.STOP_LOSS_ORDER_MAX_PERCENT = decimal.Decimal(str(0.99))\n        self.STOP_LOSS_ORDER_MIN_PERCENT = decimal.Decimal(str(0.95))\n        self.STOP_LOSS_ORDER_ATTENUATION = (self.STOP_LOSS_ORDER_MAX_PERCENT - self.STOP_LOSS_ORDER_MIN_PERCENT)\n\n        self.QUANTITY_MIN_PERCENT = decimal.Decimal(str(0.1))\n        self.QUANTITY_MAX_PERCENT = decimal.Decimal(str(0.9))\n        self.QUANTITY_ATTENUATION = (self.QUANTITY_MAX_PERCENT - self.QUANTITY_MIN_PERCENT) / self.MAX_SUM_RESULT\n\n        self.QUANTITY_MARKET_MIN_PERCENT = decimal.Decimal(str(0.3))\n        self.QUANTITY_MARKET_MAX_PERCENT = decimal.Decimal(str(1))\n        self.QUANTITY_BUY_MARKET_ATTENUATION = decimal.Decimal(str(0.2))\n        self.QUANTITY_MARKET_ATTENUATION = (self.QUANTITY_MARKET_MAX_PERCENT - self.QUANTITY_MARKET_MIN_PERCENT) \\\n                                           / self.MAX_SUM_RESULT\n\n        self.BUY_LIMIT_ORDER_MAX_PERCENT = decimal.Decimal(str(0.995))\n        self.BUY_LIMIT_ORDER_MIN_PERCENT = decimal.Decimal(str(0.98))\n        self.SELL_LIMIT_ORDER_MIN_PERCENT = 1 + (1 - self.BUY_LIMIT_ORDER_MAX_PERCENT)\n        self.SELL_LIMIT_ORDER_MAX_PERCENT = 1 + (1 - self.BUY_LIMIT_ORDER_MIN_PERCENT)\n        self.LIMIT_ORDER_ATTENUATION = (self.BUY_LIMIT_ORDER_MAX_PERCENT - self.BUY_LIMIT_ORDER_MIN_PERCENT) \\\n                                       / self.MAX_SUM_RESULT\n\n        self.QUANTITY_RISK_WEIGHT = decimal.Decimal(str(0.2))\n        self.MAX_QUANTITY_RATIO = decimal.Decimal(str(1))\n        self.MIN_QUANTITY_RATIO = decimal.Decimal(str(0.2))\n        self.DELTA_RATIO = self.MAX_QUANTITY_RATIO - self.MIN_QUANTITY_RATIO\n        # consider a high ratio not to take too much risk and not to prevent order creation either\n        self.DEFAULT_HOLDING_RATIO = decimal.Decimal(str(0.35))\n\n        self.SELL_MULTIPLIER = decimal.Decimal(str(5))\n        self.FULL_SELL_MIN_RATIO = decimal.Decimal(str(0.05))\n\n        trading_config = self.trading_mode.trading_config if self.trading_mode else {}\n\n        self.USE_TARGET_PROFIT_MODE = trading_config.get(\"target_profits_mode\", False)\n        self.USE_CLOSE_TO_CURRENT_PRICE = trading_config.get(\"use_prices_close_to_current_price\", False)\n        self.CLOSE_TO_CURRENT_PRICE_DEFAULT_RATIO = decimal.Decimal(str(\n            trading_config.get(\"close_to_current_price_difference\") or 0.02\n        ))\n        self.TARGET_PROFIT_TAKE_PROFIT = decimal.Decimal(str(\n            trading_config.get(\"target_profits_mode_take_profit\") or 5\n        )) / trading_constants.ONE_HUNDRED\n        self.USE_STOP_ORDERS = trading_config.get(\"use_stop_orders\", True)\n        self.TARGET_PROFIT_STOP_LOSS = decimal.Decimal(str(\n            trading_config.get(\"target_profits_mode_stop_loss\") or 2.5\n        )) / trading_constants.ONE_HUNDRED\n        self.TARGET_PROFIT_ENABLE_POSITION_INCREASE = trading_config.get(\n            \"target_profits_mode_enable_position_increase\", False\n        )\n        self.BUY_WITH_MAXIMUM_SIZE_ORDERS = trading_config.get(\"buy_with_maximum_size_orders\", False)\n        self.SELL_WITH_MAXIMUM_SIZE_ORDERS = trading_config.get(\"sell_with_maximum_size_orders\", False)\n        self.DISABLE_SELL_ORDERS = trading_config.get(\"disable_sell_orders\", False)\n        self.DISABLE_BUY_ORDERS = trading_config.get(\"disable_buy_orders\", False)\n        self.MAX_CURRENCY_RATIO = trading_config.get(\"max_currency_percent\", None) or None\n        if self.MAX_CURRENCY_RATIO is not None:\n            try:\n                self.MAX_CURRENCY_RATIO = decimal.Decimal(str(self.MAX_CURRENCY_RATIO)) / trading_constants.ONE_HUNDRED\n            except decimal.InvalidOperation:\n                self.MAX_CURRENCY_RATIO = None\n\n    def flush(self):\n        super().flush()\n        self.trader = None\n\n    \"\"\"\n    Starting point : self.SELL_LIMIT_ORDER_MIN_PERCENT or self.BUY_LIMIT_ORDER_MAX_PERCENT\n    1 - abs(eval_note) --> confirmation level --> high : sell less expensive / buy more expensive\n    1 - trader.risk --> high risk : sell / buy closer to the current price\n    1 - abs(eval_note) + 1 - trader.risk --> result between 0 and 2 --> self.MAX_SUM_RESULT\n    self.QUANTITY_ATTENUATION --> try to contains the result between self.XXX_MIN_PERCENT and self.XXX_MAX_PERCENT\n    \"\"\"\n\n    def _get_limit_price_from_risk(self, eval_note):\n        if eval_note > 0:\n            if self.USE_CLOSE_TO_CURRENT_PRICE:\n                return 1 + self.CLOSE_TO_CURRENT_PRICE_DEFAULT_RATIO\n            factor = self.SELL_LIMIT_ORDER_MIN_PERCENT + \\\n                     ((1 - abs(eval_note) + 1 - self.trader.risk) * self.LIMIT_ORDER_ATTENUATION)\n            return trading_modes.check_factor(self.SELL_LIMIT_ORDER_MIN_PERCENT,\n                                              self.SELL_LIMIT_ORDER_MAX_PERCENT, factor)\n        else:\n            if self.USE_CLOSE_TO_CURRENT_PRICE:\n                return 1 - self.CLOSE_TO_CURRENT_PRICE_DEFAULT_RATIO\n            factor = self.BUY_LIMIT_ORDER_MAX_PERCENT - \\\n                     ((1 - abs(eval_note) + 1 - self.trader.risk) * self.LIMIT_ORDER_ATTENUATION)\n            return trading_modes.check_factor(self.BUY_LIMIT_ORDER_MIN_PERCENT,\n                                              self.BUY_LIMIT_ORDER_MAX_PERCENT, factor)\n\n    \"\"\"\n    Starting point : self.STOP_LOSS_ORDER_MAX_PERCENT\n    trader.risk --> low risk : stop level close to the current price\n    self.STOP_LOSS_ORDER_ATTENUATION --> try to contains the result between self.STOP_LOSS_ORDER_MIN_PERCENT\n    and self.STOP_LOSS_ORDER_MAX_PERCENT\n    \"\"\"\n\n    def _get_stop_price_from_risk(self, is_long):\n        max_percent = self.STOP_LOSS_ORDER_MAX_PERCENT if is_long \\\n            else 2 * trading_constants.ONE - self.STOP_LOSS_ORDER_MIN_PERCENT\n        min_percent = self.STOP_LOSS_ORDER_MIN_PERCENT if is_long \\\n            else 2 * trading_constants.ONE - self.STOP_LOSS_ORDER_MAX_PERCENT\n        risk_difference = self.trader.risk * self.STOP_LOSS_ORDER_ATTENUATION\n        factor = max_percent - risk_difference if is_long else min_percent + risk_difference\n        return trading_modes.check_factor(min_percent, max_percent, factor)\n\n\n    async def _get_limit_quantity_from_risk(self, ctx, eval_note, max_quantity, base, selling, increasing_position):\n        order_side = trading_enums.TradeOrderSide.SELL if selling else trading_enums.TradeOrderSide.BUY\n        # check all in orders\n        if (selling and self.SELL_WITH_MAXIMUM_SIZE_ORDERS) or (not selling and self.BUY_WITH_MAXIMUM_SIZE_ORDERS):\n            return max_quantity\n        # check configured quantity\n        max_amount_from_ratio = self._get_max_amount_from_max_ratio(\n            self.MAX_CURRENCY_RATIO, max_quantity, base, self.QUANTITY_MAX_PERCENT\n        ) if increasing_position else max_quantity\n        if user_amount := trading_modes.get_user_selected_order_amount(\n            self.trading_mode,\n            self.TARGET_PROFIT_MODE_ENTRY_QUANTITY_SIDE\n            if self.USE_TARGET_PROFIT_MODE else order_side\n        ):\n            user_input_amount = await script_keywords.get_amount_from_input_amount(\n                context=ctx,\n                input_amount=user_amount,\n                side=order_side.value,\n                reduce_only=False,\n                is_stop_order=False,\n                use_total_holding=False,\n            )\n            return min(user_input_amount, max_amount_from_ratio)\n        # get quantity from risk\n        weighted_risk = self.trader.risk * self.QUANTITY_RISK_WEIGHT\n        if (\n            # consider sell quantity like a buy if base is the reference market\n            selling and base != self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n                and not increasing_position\n        ) or (\n            # consider buy quantity like a sell if quote is the reference market\n            not selling and base == self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n                and increasing_position\n        ):\n            weighted_risk *= self.SELL_MULTIPLIER\n        if not increasing_position and self._get_ratio(base) < self.FULL_SELL_MIN_RATIO:\n            return max_quantity\n        factor = self.QUANTITY_MIN_PERCENT + ((abs(eval_note) + weighted_risk) * self.QUANTITY_ATTENUATION)\n        checked_factor = trading_modes.check_factor(self.QUANTITY_MIN_PERCENT, self.QUANTITY_MAX_PERCENT, factor)\n        holding_ratio = self._get_quantity_ratio(base) if increasing_position else trading_constants.ONE\n        return min(checked_factor * max_quantity * holding_ratio, max_amount_from_ratio)\n\n    \"\"\"\n    Starting point : self.QUANTITY_MARKET_MIN_PERCENT\n    abs(eval_note) --> confirmation level --> high : sell/buy more quantity\n    trader.risk --> high risk : sell / buy more quantity\n    use SELL_MULTIPLIER to increase sell volume relatively to risk\n    abs(eval_note) + trader.risk --> result between 0 and 1 + self.QUANTITY_RISK_WEIGHT --> self.MAX_SUM_RESULT\n    self.QUANTITY_MARKET_ATTENUATION --> try to contains the result between self.QUANTITY_MARKET_MIN_PERCENT\n    and self.QUANTITY_MARKET_MAX_PERCENT\n    \"\"\"\n\n    async def _get_market_quantity_from_risk(self, ctx, eval_note, max_quantity, base, selling, increasing_position):\n        # check all in orders\n        if (selling and self.SELL_WITH_MAXIMUM_SIZE_ORDERS) or (not selling and self.BUY_WITH_MAXIMUM_SIZE_ORDERS):\n            return max_quantity\n        # check configured quantity\n        side = self.TARGET_PROFIT_MODE_ENTRY_QUANTITY_SIDE if self.USE_TARGET_PROFIT_MODE else (\n            trading_enums.TradeOrderSide.SELL if selling else trading_enums.TradeOrderSide.BUY\n        )\n        max_amount_from_ratio = (\n            self._get_max_amount_from_max_ratio(\n                self.MAX_CURRENCY_RATIO, max_quantity, base, self.QUANTITY_MARKET_MAX_PERCENT\n            ) if increasing_position else max_quantity * self.QUANTITY_MARKET_MAX_PERCENT\n        )\n        if user_amount := trading_modes.get_user_selected_order_amount(self.trading_mode, side):\n            user_input_amount = await script_keywords.get_amount_from_input_amount(\n                context=ctx,\n                input_amount=user_amount,\n                side=side.value,\n                reduce_only=False,\n                is_stop_order=False,\n                use_total_holding=False,\n            )\n            return min(user_input_amount, max_amount_from_ratio)\n        # get quantity from risk\n        weighted_risk = self.trader.risk * self.QUANTITY_RISK_WEIGHT\n        ref_market = self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n        if (not increasing_position and base != ref_market) or (increasing_position and base == ref_market):\n            weighted_risk *= self.SELL_MULTIPLIER\n        factor = self.QUANTITY_MARKET_MIN_PERCENT + (abs(eval_note) + weighted_risk) * self.QUANTITY_MARKET_ATTENUATION\n        checked_factor = trading_modes.check_factor(\n            self.QUANTITY_MARKET_MIN_PERCENT, self.QUANTITY_MARKET_MAX_PERCENT, factor\n        )\n        holding_ratio = 1 if not increasing_position else self._get_quantity_ratio(base)\n        return min(checked_factor * holding_ratio * max_quantity, max_amount_from_ratio)\n\n    def _get_ratio(self, currency):\n        try:\n            return self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                portfolio_value_holder.get_holdings_ratio(currency)\n        except trading_errors.MissingPriceDataError:\n            # Can happen when ref market is not in the pair, data will be available later (ticker is now registered)\n            return self.DEFAULT_HOLDING_RATIO\n\n    def _get_quantity_ratio(self, currency):\n        if self.get_number_of_traded_assets() > 2:\n            ratio = self._get_ratio(currency)\n            # returns a linear result between self.MIN_QUANTITY_RATIO and self.MAX_QUANTITY_RATIO: closer to\n            # self.MAX_QUANTITY_RATIO when holdings are lower in % and to self.MIN_QUANTITY_RATIO when holdings\n            # are higher in %\n            return 1 - min(ratio * self.DELTA_RATIO, 1)\n        else:\n            return 1\n\n    def _get_max_amount_from_max_ratio(self, max_ratio, quantity, currency, default_ratio):\n        # TODO ratios in futures trading\n        # reduce max amount when self.MAX_CURRENCY_RATIO is defined\n        if self.MAX_CURRENCY_RATIO is None or max_ratio == trading_constants.ONE or self.exchange_manager.is_future:\n            return quantity * default_ratio\n        max_amount_ratio = max_ratio - self._get_ratio(currency)\n        if max_amount_ratio > 0:\n            max_amount_in_ref_market = trading_api.get_current_portfolio_value(self.exchange_manager) * \\\n                                       max_amount_ratio\n            try:\n                max_theoretical_amount = max_amount_in_ref_market / trading_api.get_current_crypto_currency_value(\n                    self.exchange_manager, currency)\n                return min(max_theoretical_amount, quantity)\n            except KeyError:\n                self.logger.error(f\"Missing price information in reference market for {currency}. Skipping buy order \"\n                                  f\"as is it required to ensure the maximum currency percent parameter. \"\n                                  f\"Set it to 100 to buy anyway.\")\n        return trading_constants.ZERO\n\n    def _get_split_take_profit_details(\n        self, order_details: list[OrderDetails], total_quantity: decimal.Decimal, symbol_market\n    ):\n        prices = [order_detail.price for order_detail in order_details]\n        amount_ratio_per_order = [\n            order_detail.quantity\n            for order_detail in order_details\n            if order_detail.quantity is not None\n        ]\n        quantities, prices = trading_personal_data.get_valid_split_orders(\n            total_quantity, prices, symbol_market, amount_ratio_per_order=amount_ratio_per_order\n        )\n        return [\n            OrderDetails(price, quantity)\n            for quantity, price in zip(quantities, prices)\n        ]\n\n    async def _create_order(\n        self, current_order,\n        use_take_profit_orders, take_profits_details: list[OrderDetails],\n        use_stop_loss_orders, stop_loss_details: list[OrderDetails],\n        symbol_market, tag,\n        trailing_profile_type: typing.Optional[trading_personal_data.TrailingProfileTypes],\n        active_order_swap_strategy: trading_personal_data.ActiveOrderSwapStrategy,\n        dependencies: typing.Optional[signals.SignalDependencies],\n    ):\n        params = {}\n        chained_orders = []\n        is_long = current_order.side is trading_enums.TradeOrderSide.BUY\n        exit_side = trading_enums.TradeOrderSide.SELL if is_long else trading_enums.TradeOrderSide.BUY\n        # tag chained orders as reduce_only when trading futures\n        reduce_only_chained_orders = self.exchange_manager.is_future\n        if use_stop_loss_orders:\n            if len(stop_loss_details) > 1:\n                self.logger.error(f\"Multiple stop loss orders is not supported.\")\n            stop_price = trading_personal_data.decimal_adapt_price(\n                symbol_market,\n                current_order.origin_price * (\n                    trading_constants.ONE + (self.TARGET_PROFIT_STOP_LOSS * (-1 if is_long else 1))\n                )\n            ) if (not stop_loss_details or stop_loss_details[0].price.is_nan()) else stop_loss_details[0].price\n            param_update, chained_order = await self.register_chained_order(\n                current_order, stop_price, trading_enums.TraderOrderType.STOP_LOSS, exit_side,\n                tag=tag, reduce_only=reduce_only_chained_orders\n            )\n            params.update(param_update)\n            chained_orders.append(chained_order)\n        if use_take_profit_orders:\n            if take_profits_details:\n                local_take_profits_details = self._get_split_take_profit_details(\n                    take_profits_details, current_order.origin_quantity, symbol_market\n                )\n            else:\n                local_take_profits_details = [\n                    OrderDetails(decimal.Decimal(\"nan\"), current_order.origin_quantity)\n                ]\n            for index, take_profits_detail in enumerate(local_take_profits_details):\n                is_last = index == len(local_take_profits_details) - 1\n                take_profit_price = trading_personal_data.decimal_adapt_price(\n                    symbol_market,\n                    current_order.origin_price * (\n                        trading_constants.ONE + (self.TARGET_PROFIT_TAKE_PROFIT * (1 if is_long else -1))\n                    )\n                ) if take_profits_detail.price.is_nan() else take_profits_detail.price\n                order_type = self.exchange_manager.trader.get_take_profit_order_type(\n                    current_order,\n                    trading_enums.TraderOrderType.SELL_LIMIT if exit_side is trading_enums.TradeOrderSide.SELL\n                    else trading_enums.TraderOrderType.BUY_LIMIT\n                )\n                param_update, chained_order = await self.register_chained_order(\n                    current_order, take_profit_price, order_type, exit_side,\n                    quantity=take_profits_detail.quantity, tag=tag, reduce_only=reduce_only_chained_orders,\n                    # only the last order is to take trigger fees into account\n                    update_with_triggering_order_fees=is_last and not self.exchange_manager.is_future\n                )\n                params.update(param_update)\n                chained_orders.append(chained_order)\n        stop_orders = [o for o in chained_orders if trading_personal_data.is_stop_order(o.order_type)]\n        tp_orders = [o for o in chained_orders if not trading_personal_data.is_stop_order(o.order_type)]\n        if stop_orders and tp_orders:\n            if len(stop_orders) == len(tp_orders):\n                group_type = trading_personal_data.OneCancelsTheOtherOrderGroup\n            elif trailing_profile_type == trading_personal_data.TrailingProfileTypes.FILLED_TAKE_PROFIT:\n                group_type = trading_personal_data.TrailingOnFilledTPBalancedOrderGroup\n                entry_price = current_order.origin_price\n                for stop_order in stop_orders:\n                    # register trailing profile in stop orders\n                    stop_order.trailing_profile = trading_personal_data.create_filled_take_profit_trailing_profile(\n                        entry_price, tp_orders\n                    )\n            else:\n                group_type = trading_personal_data.BalancedTakeProfitAndStopOrderGroup\n            oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(\n                group_type, active_order_swap_strategy=active_order_swap_strategy\n            )\n            for order in chained_orders:\n                order.add_to_order_group(oco_group)\n            # in futures, inactive orders are not necessary\n            if self.exchange_manager.trader.enable_inactive_orders and not self.exchange_manager.is_future:\n                await oco_group.active_order_swap_strategy.apply_inactive_orders(chained_orders)\n        return await self.trading_mode.create_order(\n            current_order, params=params or None, dependencies=dependencies\n        )\n\n    async def create_new_orders(self, symbol, final_note, state, **kwargs):\n        try:\n            if final_note.is_nan():\n                return []\n        except AttributeError:\n            final_note = decimal.Decimal(str(final_note))\n            if final_note.is_nan():\n                return []\n        data = kwargs.get(self.CREATE_ORDER_DATA_PARAM, {})\n        dependencies = kwargs.get(self.CREATE_ORDER_DEPENDENCIES_PARAM, None)\n        user_price = data.get(self.PRICE_KEY, trading_constants.ZERO)\n        user_volume = data.get(self.VOLUME_KEY, trading_constants.ZERO)\n        user_reduce_only = data.get(self.REDUCE_ONLY_KEY, False) if self.exchange_manager.is_future else None\n        tag = data.get(self.TAG_KEY, None)\n        exchange_creation_params = data.get(self.ORDER_EXCHANGE_CREATION_PARAMS, {})\n        current_order = None\n        orders_should_have_been_created = False\n        timeout = kwargs.pop(\"timeout\", trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n        ctx = script_keywords.get_base_context(self.trading_mode, symbol)\n        try:\n            current_symbol_holding, current_market_holding, market_quantity, price, symbol_market = \\\n                await trading_personal_data.get_pre_order_data(self.exchange_manager, symbol=symbol, timeout=timeout)\n            self.logger.debug(\n                f\"Order creation inputs: \"\n                f\"current_symbol_holding: {current_symbol_holding}, \"\n                f\"current_market_holding: {current_market_holding}, \"\n                f\"market_quantity: {market_quantity}, \"\n                f\"price: {price}.\"\n            )\n            max_buy_size = market_quantity\n            max_sell_size = current_symbol_holding\n            spot_increasing_position = state in (trading_enums.EvaluatorStates.VERY_LONG.value,\n                                                 trading_enums.EvaluatorStates.LONG.value)\n            if self.exchange_manager.is_future:\n                self.trading_mode.ensure_supported(symbol)\n                # on futures, current_symbol_holding = current_market_holding = market_quantity\n                max_buy_size, buy_increasing_position = trading_personal_data.get_futures_max_order_size(\n                    self.exchange_manager, symbol, trading_enums.TradeOrderSide.BUY,\n                    price, False, current_symbol_holding, market_quantity\n                )\n                max_sell_size, sell_increasing_position = trading_personal_data.get_futures_max_order_size(\n                    self.exchange_manager, symbol, trading_enums.TradeOrderSide.SELL,\n                    price, False, current_symbol_holding, market_quantity\n                )\n                # take the right value depending on if we are in a buy or sell condition\n                increasing_position = buy_increasing_position if spot_increasing_position else sell_increasing_position\n            else:\n                increasing_position = spot_increasing_position\n\n            base = symbol_util.parse_symbol(symbol).base\n            created_orders = []\n            # use stop loss when reducing the position and stop are enabled or when the user explicitly asks for one\n            user_take_profit_price = trading_personal_data.decimal_adapt_price(\n                symbol_market,\n                data.get(self.TAKE_PROFIT_PRICE_KEY, decimal.Decimal(math.nan))\n            )\n            additional_user_take_profit_prices = [\n                trading_personal_data.decimal_adapt_price(\n                    symbol_market,\n                    price\n                )\n                for price in (data.get(self.ADDITIONAL_TAKE_PROFIT_PRICES_KEY) or [])\n            ]\n            additional_user_take_profit_volume_ratios = (\n                list(data.get(self.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY) or [])\n            )\n            if additional_user_take_profit_volume_ratios:\n                expected_volumes = 0\n                if (not user_take_profit_price.is_nan()) and additional_user_take_profit_prices:\n                    expected_volumes = len(additional_user_take_profit_prices) + 1\n                elif additional_user_take_profit_prices:\n                    expected_volumes = len(additional_user_take_profit_prices)\n                if expected_volumes and len(additional_user_take_profit_volume_ratios) != expected_volumes:\n                    raise ValueError(\n                        f\"{self.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY} must have a size\"\n                        f\" of {expected_volumes}. \"\n                        f\"{len(data[self.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY])=} \"\n                        f\"{len(data[self.ADDITIONAL_TAKE_PROFIT_PRICES_KEY])=}\"\n                    )\n            user_stop_price = trading_personal_data.decimal_adapt_price(\n                symbol_market,\n                data.get(self.STOP_PRICE_KEY, decimal.Decimal(math.nan))\n            )\n            create_stop_only = data.get(self.STOP_ONLY, False)\n            if create_stop_only and (not user_stop_price or user_stop_price.is_nan()):\n                self.logger.error(\"Stop price is required to create a stop order\")\n                return []\n            trailing_profile_type = trading_personal_data.TrailingProfileTypes(\n                data[self.TRAILING_PROFILE]\n            ) if data.get(self.TRAILING_PROFILE) else None\n            cancel_policy = trading_personal_data.create_cancel_policy(\n                data.get(self.CANCEL_POLICY), data.get(self.CANCEL_POLICY_PARAMS)\n            ) if data.get(self.CANCEL_POLICY) else None\n            is_reducing_position = not increasing_position\n            if self.USE_TARGET_PROFIT_MODE:\n                if is_reducing_position:\n                    self.logger.debug(\"Ignored reducing position signal as Target Profit Mode is enabled. \"\n                                      \"Positions are reduced from chained orders that are created at entry time.\")\n                    return []\n                elif not self.TARGET_PROFIT_ENABLE_POSITION_INCREASE:\n                    if self.exchange_manager.is_future:\n                        current_position = self.exchange_manager.exchange_personal_data.positions_manager\\\n                            .get_symbol_position(\n                                symbol,\n                                trading_enums.PositionSide.BOTH\n                            )\n                        if not current_position.is_idle():\n                            self.logger.debug(\n                                f\"Ignored increasing position signal on {symbol} as Mode 'Enable futures \"\n                                f\"position increase' is disabled.\"\n                            )\n                            return []\n\n            use_stop_orders = is_reducing_position and (self.USE_STOP_ORDERS or not user_stop_price.is_nan())\n            # use stop loss when increasing the position and the user explicitly asks for one\n            use_chained_take_profit_orders = increasing_position and (\n                (not user_take_profit_price.is_nan() or additional_user_take_profit_prices)\n                or self.USE_TARGET_PROFIT_MODE\n            )\n            use_chained_stop_loss_orders = increasing_position and (\n                not user_stop_price.is_nan() or (self.USE_TARGET_PROFIT_MODE and self.USE_STOP_ORDERS)\n            )\n            stop_loss_order_details = take_profit_order_details = []\n            if use_chained_take_profit_orders:\n                take_profit_order_details = [] if user_take_profit_price.is_nan() else [\n                    OrderDetails(\n                        user_take_profit_price,\n                        additional_user_take_profit_volume_ratios[0] if additional_user_take_profit_volume_ratios\n                        else None\n                    )\n                ]\n                used_first_volume = not user_take_profit_price.is_nan()\n                take_profit_order_details += [\n                    OrderDetails(\n                        price,\n                        additional_user_take_profit_volume_ratios[index + (1 if used_first_volume else 0)]\n                        if additional_user_take_profit_volume_ratios\n                        else None\n                    )\n                    for index, price in enumerate(additional_user_take_profit_prices)\n                ]\n            if use_chained_stop_loss_orders:\n                stop_loss_order_details = [OrderDetails(user_stop_price, None)]\n\n\n            active_order_swap_strategy = data.get(self.ACTIVE_ORDER_SWAP_STRATEGY) or (\n                trading_personal_data.StopFirstActiveOrderSwapStrategy(data.get(\n                    self.ACTIVE_ORDER_SWAP_TIMEOUT, trading_constants.ACTIVE_ORDER_STRATEGY_SWAP_TIMEOUT\n                ))\n            )\n\n            if state == trading_enums.EvaluatorStates.VERY_SHORT.value and not self.DISABLE_SELL_ORDERS:\n                quantity = user_volume \\\n                           or await self._get_market_quantity_from_risk(\n                    ctx, final_note, max_sell_size, base, True, increasing_position\n                )\n                quantity = trading_personal_data.decimal_add_dusts_to_quantity_if_necessary(quantity, price,\n                                                                                            symbol_market,\n                                                                                            max_sell_size)\n\n                for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                        quantity,\n                        price,\n                        symbol_market):\n                    orders_should_have_been_created = True\n                    current_order = trading_personal_data.create_order_instance(\n                        trader=self.trader,\n                        order_type=trading_enums.TraderOrderType.SELL_MARKET,\n                        symbol=symbol,\n                        current_price=order_price,\n                        quantity=order_quantity,\n                        price=order_price,\n                        reduce_only=user_reduce_only,\n                        exchange_creation_params=exchange_creation_params,\n                        tag=tag,\n                        cancel_policy=cancel_policy,\n                    )\n                    if current_order := await self._create_order(\n                        current_order,\n                        use_chained_take_profit_orders, take_profit_order_details,\n                        use_chained_stop_loss_orders, stop_loss_order_details,\n                        symbol_market, tag,\n                        trailing_profile_type,\n                        active_order_swap_strategy,\n                        dependencies\n                    ):\n                        created_orders.append(current_order)\n\n            elif state == trading_enums.EvaluatorStates.SHORT.value and not self.DISABLE_SELL_ORDERS:\n                quantity = user_volume or await self._get_limit_quantity_from_risk(\n                    ctx, final_note, max_sell_size, base, True, increasing_position\n                )\n                quantity = trading_personal_data.decimal_add_dusts_to_quantity_if_necessary(quantity, price,\n                                                                                            symbol_market,\n                                                                                            max_sell_size)\n                limit_price = user_stop_price if create_stop_only else trading_personal_data.decimal_adapt_price(\n                    symbol_market, user_price or (price * self._get_limit_price_from_risk(final_note))\n                )\n                for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    quantity,\n                    limit_price,\n                    symbol_market\n                ):\n                    orders_should_have_been_created = True\n                    current_stop_order = None\n                    current_limit_order = trading_personal_data.create_order_instance(\n                        trader=self.trader,\n                        order_type=trading_enums.TraderOrderType.SELL_LIMIT,\n                        symbol=symbol,\n                        current_price=price,\n                        quantity=order_quantity,\n                        price=order_price,\n                        reduce_only=user_reduce_only,\n                        exchange_creation_params=exchange_creation_params,\n                        tag=tag,\n                        cancel_policy=cancel_policy,\n                    )\n                    if create_stop_only or use_stop_orders:\n                        oco_group = None\n                        if not create_stop_only:\n                            oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(\n                                trading_personal_data.OneCancelsTheOtherOrderGroup,\n                                active_order_swap_strategy=active_order_swap_strategy\n                            )\n                            current_limit_order.add_to_order_group(oco_group)\n                        stop_price = trading_personal_data.decimal_adapt_price(\n                            symbol_market, price * self._get_stop_price_from_risk(True)\n                        ) if user_stop_price.is_nan() else user_stop_price\n                        current_stop_order = trading_personal_data.create_order_instance(\n                            trader=self.trader,\n                            order_type=trading_enums.TraderOrderType.STOP_LOSS,\n                            symbol=symbol,\n                            current_price=price,\n                            quantity=order_quantity,\n                            price=stop_price,\n                            side=trading_enums.TradeOrderSide.SELL,\n                            reduce_only=True,\n                            group=oco_group,\n                            exchange_creation_params=exchange_creation_params,\n                            tag=tag,\n                            cancel_policy=cancel_policy if create_stop_only else None,\n                        )\n                        # in futures, inactive orders are not necessary\n                        if (\n                            oco_group and self.exchange_manager.trader.enable_inactive_orders\n                            and not self.exchange_manager.is_future\n                        ):\n                            await oco_group.active_order_swap_strategy.apply_inactive_orders(\n                                [current_limit_order, current_stop_order]\n                            )\n                    # now create orders on exchange\n                    created_limit = None\n                    if not create_stop_only:\n                        created_limit = await self._create_order(\n                            current_limit_order,\n                            use_chained_take_profit_orders, take_profit_order_details,\n                            use_chained_stop_loss_orders, stop_loss_order_details,\n                            symbol_market, tag,\n                            trailing_profile_type,\n                            active_order_swap_strategy,\n                            dependencies\n                        )\n                        created_orders.append(created_limit)\n                    if current_stop_order is not None and (create_stop_only or (\n                        created_limit is not None and created_limit.is_open()\n                    )):\n                        created_stop = await self.trading_mode.create_order(\n                            current_stop_order, dependencies=dependencies\n                        )\n                        if create_stop_only:\n                            created_orders.append(created_stop)\n\n            elif state == trading_enums.EvaluatorStates.NEUTRAL.value:\n                return []\n\n            elif state == trading_enums.EvaluatorStates.LONG.value and not self.DISABLE_BUY_ORDERS:\n                quantity = await self._get_limit_quantity_from_risk(\n                    ctx, final_note, max_buy_size, base, False, increasing_position\n                ) if user_volume == 0 else user_volume\n                limit_price = user_stop_price if create_stop_only else trading_personal_data.decimal_adapt_price(\n                    symbol_market, user_price or (price * self._get_limit_price_from_risk(final_note))\n                )\n                quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                    self.exchange_manager, symbol, trading_enums.TraderOrderType.BUY_LIMIT, quantity,\n                    limit_price, trading_enums.TradeOrderSide.BUY\n                )\n                for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    quantity,\n                    limit_price,\n                    symbol_market\n                ):\n                    orders_should_have_been_created = True\n                    current_stop_order = None\n                    current_limit_order = trading_personal_data.create_order_instance(\n                        trader=self.trader,\n                        order_type=trading_enums.TraderOrderType.BUY_LIMIT,\n                        symbol=symbol,\n                        current_price=price,\n                        quantity=order_quantity,\n                        price=order_price,\n                        reduce_only=user_reduce_only,\n                        exchange_creation_params=exchange_creation_params,\n                        tag=tag,\n                        cancel_policy=cancel_policy,\n                    )\n                    if create_stop_only or use_stop_orders:\n                        oco_group = None\n                        if not create_stop_only:\n                            oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(\n                                trading_personal_data.OneCancelsTheOtherOrderGroup,\n                                active_order_swap_strategy=active_order_swap_strategy\n                            )\n                            current_limit_order.add_to_order_group(oco_group)\n                        stop_price = trading_personal_data.decimal_adapt_price(\n                            symbol_market, price * self._get_stop_price_from_risk(False)\n                        ) if user_stop_price.is_nan() else user_stop_price\n                        current_stop_order = trading_personal_data.create_order_instance(\n                            trader=self.trader,\n                            order_type=trading_enums.TraderOrderType.STOP_LOSS,\n                            symbol=symbol,\n                            current_price=price,\n                            quantity=order_quantity,\n                            price=stop_price,\n                            side=trading_enums.TradeOrderSide.BUY,\n                            reduce_only=True,\n                            group=oco_group,\n                            exchange_creation_params=exchange_creation_params,\n                            tag=tag,\n                            cancel_policy=cancel_policy if create_stop_only else None,\n                        )\n                        # in futures, inactive orders are not necessary\n                        if (\n                            oco_group and self.exchange_manager.trader.enable_inactive_orders\n                            and not self.exchange_manager.is_future\n                        ):\n                            await oco_group.active_order_swap_strategy.apply_inactive_orders(\n                                [current_limit_order, current_stop_order]\n                            )\n                    # now create orders on exchange\n                    created_limit = None\n                    if not create_stop_only:\n                        created_limit = await self._create_order(\n                            current_limit_order,\n                            use_chained_take_profit_orders, take_profit_order_details,\n                            use_chained_stop_loss_orders, stop_loss_order_details,\n                            symbol_market, tag,\n                            trailing_profile_type,\n                            active_order_swap_strategy,\n                            dependencies\n                        )\n                        created_orders.append(created_limit)\n                    if current_stop_order is not None and (create_stop_only or (\n                        created_limit is not None and created_limit.is_open()\n                    )):\n                        created_stop = await self.trading_mode.create_order(\n                            current_stop_order, dependencies=dependencies\n                        )\n                        if create_stop_only:\n                            created_orders.append(created_stop)\n\n            elif state == trading_enums.EvaluatorStates.VERY_LONG.value and not self.DISABLE_BUY_ORDERS:\n                quantity = await self._get_market_quantity_from_risk(\n                    ctx, final_note, max_buy_size, base, False, increasing_position\n                ) \\\n                    if user_volume == 0 else user_volume\n                quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                    self.exchange_manager, symbol, trading_enums.TraderOrderType.BUY_MARKET, quantity,\n                    price, trading_enums.TradeOrderSide.BUY\n                )\n                for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    quantity,\n                    price,\n                    symbol_market\n                ):\n                    orders_should_have_been_created = True\n                    current_order = trading_personal_data.create_order_instance(\n                        trader=self.trader,\n                        order_type=trading_enums.TraderOrderType.BUY_MARKET,\n                        symbol=symbol,\n                        current_price=order_price,\n                        quantity=order_quantity,\n                        price=order_price,\n                        reduce_only=user_reduce_only,\n                        exchange_creation_params=exchange_creation_params,\n                        tag=tag,\n                        cancel_policy=cancel_policy,\n                    )\n                    if current_order := await self._create_order(\n                        current_order,\n                        use_chained_take_profit_orders, take_profit_order_details,\n                        use_chained_stop_loss_orders, stop_loss_order_details,\n                        symbol_market, tag,\n                        trailing_profile_type,\n                        active_order_swap_strategy,\n                        dependencies\n                    ):\n                        created_orders.append(current_order)\n            if created_orders:\n                return created_orders\n            if orders_should_have_been_created:\n                raise trading_errors.OrderCreationError()\n            raise trading_errors.MissingMinimalExchangeTradeVolume()\n\n        except (\n            trading_errors.MissingFunds,\n            trading_errors.MissingMinimalExchangeTradeVolume,\n            trading_errors.OrderCreationError,\n            trading_errors.InvalidPositionSide,\n            trading_errors.UnsupportedContractConfigurationError,\n            trading_errors.InvalidCancelPolicyError\n        ):\n            raise\n        except asyncio.TimeoutError as e:\n            self.logger.error(f\"Impossible to create order for {symbol} on {self.exchange_manager.exchange_name}: {e} \"\n                              f\"and is necessary to compute the order details.\")\n            return []\n        except Exception as e:\n            self.logger.exception(e, True, f\"Failed to create order : {e}.\")\n            return []\n\n\nclass DailyTradingModeProducer(trading_modes.AbstractTradingModeProducer):\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n\n        self.state = None\n\n        # If final_eval not is < X_THRESHOLD --> state = X\n        self.VERY_LONG_THRESHOLD = decimal.Decimal(\"-0.85\")\n        self.LONG_THRESHOLD = decimal.Decimal(\"-0.25\")\n        self.NEUTRAL_THRESHOLD = decimal.Decimal(\"0.25\")\n        self.SHORT_THRESHOLD = decimal.Decimal(\"0.85\")\n        self.RISK_THRESHOLD = decimal.Decimal(\"0.2\")\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        await super().stop()\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        strategies_analysis_note_counter = 0\n        evaluation = commons_constants.INIT_EVAL_NOTE\n        # Strategies analysis\n        for evaluated_strategy_node in matrix.get_tentacles_value_nodes(\n                matrix_id,\n                matrix.get_tentacle_nodes(matrix_id,\n                                          exchange_name=self.exchange_name,\n                                          tentacle_type=evaluators_enums.EvaluatorMatrixTypes.STRATEGIES.value),\n                cryptocurrency=cryptocurrency,\n                symbol=symbol):\n\n            if evaluators_util.check_valid_eval_note(evaluators_api.get_value(evaluated_strategy_node),\n                                                     evaluators_api.get_type(evaluated_strategy_node),\n                                                     evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE):\n                evaluation += evaluators_api.get_value(\n                    evaluated_strategy_node\n                )\n                strategies_analysis_note_counter += 1\n\n        if strategies_analysis_note_counter > 0:\n            self.final_eval = decimal.Decimal(str(evaluation / strategies_analysis_note_counter))\n            await self.create_state(cryptocurrency=cryptocurrency, symbol=symbol)\n\n    def _get_delta_risk(self):\n        return self.RISK_THRESHOLD * self.exchange_manager.trader.risk\n\n    async def create_state(self, cryptocurrency: str, symbol: str):\n        if self.final_eval.is_nan():\n            # discard NaN case as it is not usable\n            await self._set_state(cryptocurrency=cryptocurrency,\n                                  symbol=symbol,\n                                  new_state=trading_enums.EvaluatorStates.NEUTRAL)\n            return\n        delta_risk = self._get_delta_risk()\n        if self.final_eval < self.VERY_LONG_THRESHOLD + delta_risk:\n            await self._set_state(cryptocurrency=cryptocurrency,\n                                  symbol=symbol,\n                                  new_state=trading_enums.EvaluatorStates.VERY_LONG)\n        elif self.final_eval < self.LONG_THRESHOLD + delta_risk:\n            await self._set_state(cryptocurrency=cryptocurrency,\n                                  symbol=symbol,\n                                  new_state=trading_enums.EvaluatorStates.LONG)\n        elif self.final_eval < self.NEUTRAL_THRESHOLD - delta_risk:\n            await self._set_state(cryptocurrency=cryptocurrency,\n                                  symbol=symbol,\n                                  new_state=trading_enums.EvaluatorStates.NEUTRAL)\n        elif self.final_eval < self.SHORT_THRESHOLD - delta_risk:\n            await self._set_state(cryptocurrency=cryptocurrency,\n                                  symbol=symbol,\n                                  new_state=trading_enums.EvaluatorStates.SHORT)\n        else:\n            await self._set_state(cryptocurrency=cryptocurrency,\n                                  symbol=symbol,\n                                  new_state=trading_enums.EvaluatorStates.VERY_SHORT)\n\n    @classmethod\n    def get_should_cancel_loaded_orders(cls):\n        return True\n\n    async def _set_state(self, cryptocurrency: str, symbol: str, new_state):\n        if new_state != self.state:\n            self.state = new_state\n            self.logger.info(f\"[{symbol}] new state: {self.state.name}\")\n\n            # if new state is not neutral --> cancel orders and create new else keep orders\n            if new_state is not trading_enums.EvaluatorStates.NEUTRAL:\n                _, dependencies = await self.apply_cancel_policies()\n                if self.trading_mode.consumers:\n                    if self.trading_mode.consumers[0].USE_TARGET_PROFIT_MODE:\n                        new_dependencies = await self._cancel_position_opening_orders(symbol)\n                    else:\n                        # cancel open orders when not on target profit mode\n                        _, new_dependencies = await self.cancel_symbol_open_orders(symbol)\n                    if new_dependencies:\n                        if dependencies:\n                            dependencies.extend(new_dependencies)\n                        else:\n                            dependencies = new_dependencies\n\n                # call orders creation from consumers\n                await self.submit_trading_evaluation(cryptocurrency=cryptocurrency,\n                                                     symbol=symbol,\n                                                     time_frame=None,\n                                                     final_note=self.final_eval,\n                                                     state=self.state,\n                                                     dependencies=dependencies)\n\n                # send_notification\n                if not self.exchange_manager.is_backtesting:\n                    await self._send_alert_notification(symbol, new_state)\n\n    async def _cancel_position_opening_orders(self, symbol) -> signals.SignalDependencies:\n        dependencies = signals.SignalDependencies()\n        if self.exchange_manager.trader.is_enabled:\n            for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol):\n                if (\n                    not (order.is_cancelled() or order.is_closed())\n                    # orders with chained orders and no \"triggered by\" are \"position opening\"\n                    and order.chained_orders and order.triggered_by is None\n                ):\n                    try:\n                        is_cancelled, dependency = await self.trading_mode.cancel_order(order)\n                        if is_cancelled:\n                            dependencies.extend(dependency)\n                    except trading_errors.UnexpectedExchangeSideOrderStateError as err:\n                        self.logger.warning(f\"Skipped order cancel: {err}, order: {order}\")\n        return dependencies\n\n    async def _send_alert_notification(self, symbol, new_state):\n        try:\n            import octobot_services.api as services_api\n            import octobot_services.enums as services_enum\n            title = f\"OCTOBOT ALERT : #{symbol}\"\n            alert_content, alert_content_markdown = pretty_printer.cryptocurrency_alert(\n                new_state,\n                self.final_eval)\n            await services_api.send_notification(services_api.create_notification(alert_content, title=title,\n                                                                                  markdown_text=alert_content_markdown,\n                                                                                  category=services_enum.NotificationCategory.PRICE_ALERTS))\n        except ImportError as e:\n            self.logger.exception(e, True, f\"Impossible to send notification: {e}\")\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"DailyTradingMode\"],\n  \"tentacles-requirements\": [\"mixed_strategies_evaluator\"]\n}"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/resources/DailyTradingMode.md",
    "content": "The DailyTradingMode will consider every compatible strategy and evaluator and average their evaluation to create\neach update.\n\nIt will create orders when its state changes to \na state that is different from the previous one and that is not NEUTRAL.\n\nA LONG state will trigger a buy order. A SHORT state will trigger a sell order. \n\n<div class=\"text-center\">\n    <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/e-GqmTfrchY?showinfo=0&amp;rel=0\" \n    title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; \n    clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</div>\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/daily-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=DailyTradingModeDocs\">\nfull Daily trading mode guide</a>.\n\n### Default mode\nOn Default mode, the DailyTradingMode will cancel previously created open orders \nand create new ones according to its new state. \nIn this mode, both buy and sell orders will be exclusively created upon strategy and evaluator signals.\n\n### Target profits mode\nOn Target profits mode, the DailyTradingMode will only listen for LONG signals when trading spot \nand position-increasing signals when trading futures, which means both SHORT and LONG. When such a signal is received, it will create an entry order \nthat will be followed by a take profit (and possibly a stop-loss) when filled. In this mode, only entry signals are \ndefined by your strategy and evaluator configuration as take profit and stop loss targets are defined in \nthe Target profits mode configuration.  \n*Using the DailyTradingMode in Target profits mode is compatible with PNL history.*\n\n### About futures trading  \nThe **Target profits** mode is more adapted to futures trading as it creates take profits and stop losses (when enabled) \nto close created positions.\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\n\nimport octobot_commons.tests.test_config as test_config\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport tests.test_utils.memory_check_util as memory_check_util\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Evaluator.TA as Evaluator\nimport tentacles.Trading.Mode as Mode\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_run_independent_backtestings_with_memory_check():\n    tentacles_setup_config = tentacles_manager_api.create_tentacles_setup_config_with_tentacles(Mode.DailyTradingMode,\n                                                                                                Strategies.SimpleStrategyEvaluator,\n                                                                                                Evaluator.RSIMomentumEvaluator,\n                                                                                                Evaluator.DoubleMovingAverageTrendEvaluator)\n    await memory_check_util.run_independent_backtestings_with_memory_check(test_config.load_test_config(),\n                                                                           tentacles_setup_config)\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport math\nimport mock\nimport pytest\nimport os.path\nimport copy\nimport pytest_asyncio\n\nimport async_channel.util as channel_util\nimport octobot_backtesting.api as backtesting_api\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.tests.test_config as test_config\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchange_data as exchange_data\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.modes.script_keywords as script_keywords\nimport octobot_trading.personal_data as trading_personal_data\nimport tentacles.Trading.Mode as Mode\nimport tests.unit_tests.trading_modes_tests.trading_mode_test_toolkit as trading_mode_test_toolkit\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def tools():\n    tentacles_manager_api.reload_tentacle_info()\n    exchange_manager = None\n    try:\n        symbol = \"BTC/USDT\"\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"SUB\"] = 0.000000000000000000005\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"BNB\"] = 0.000000000000000000005\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        mode = Mode.DailyTradingMode(config, exchange_manager)\n        await mode.initialize()\n        # add mode to exchange manager so that it can be stopped and freed from memory\n        exchange_manager.trading_modes.append(mode)\n        consumer = mode.get_trading_mode_consumers()[0]\n        consumer.MAX_CURRENCY_RATIO = 1\n\n        # set BTC/USDT price at 7009.194999999998 USDT\n        last_btc_price = 7009.194999999998\n        trading_api.force_set_mark_price(exchange_manager, symbol, last_btc_price)\n\n        yield exchange_manager, trader, symbol, consumer, decimal.Decimal(str(last_btc_price))\n    finally:\n        if exchange_manager:\n            try:\n                await _stop(exchange_manager)\n            except Exception as err:\n                print(f\"error when stopping exchange manager: {err}\")\n\n\n@pytest_asyncio.fixture\nasync def future_tools():\n    tentacles_manager_api.reload_tentacle_info()\n    exchange_manager = None\n    try:\n        symbol = \"BTC/USDT:USDT\"\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"SUB\"] = 0.000000000000000000005\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"BNB\"] = 0.000000000000000000005\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_spot_only = False\n        exchange_manager.is_future = True\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        contract = exchange_data.FutureContract(\n            pair=symbol,\n            margin_type=trading_enums.MarginType.ISOLATED,\n            contract_type=trading_enums.FutureContractType.LINEAR_PERPETUAL,\n            current_leverage=trading_constants.ONE,\n            maximum_leverage=trading_constants.ONE_HUNDRED\n        )\n        exchange_manager.exchange.set_pair_future_contract(symbol, contract)\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        mode = Mode.DailyTradingMode(config, exchange_manager)\n        await mode.initialize()\n        # add mode to exchange manager so that it can be stopped and freed from memory\n        exchange_manager.trading_modes.append(mode)\n        consumer = mode.get_trading_mode_consumers()[0]\n        consumer.MAX_CURRENCY_RATIO = 1\n\n        # set BTC/USDT:USDT price at 7009.194999999998 USDT\n        last_btc_price = 7009.194999999998\n        trading_api.force_set_mark_price(exchange_manager, symbol, last_btc_price)\n\n        yield exchange_manager, trader, symbol, consumer, decimal.Decimal(str(last_btc_price))\n    finally:\n        if exchange_manager:\n            try:\n                await _stop(exchange_manager)\n            except Exception as err:\n                print(f\"error when stopping exchange manager: {err}\")\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n    # let updaters gracefully shutdown\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def test_valid_create_new_orders_no_ref_market_as_quote(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # change reference market to USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.reference_market = \"USDT\"\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\n        symbol] = last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(last_btc_price * 10 + 1000))\n\n    market_status = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n\n    # portfolio: \"BTC\": 10 \"USD\": 1000\n    # order from neutral state\n    _origin_decimal_adapt_order_quantity_because_fees = trading_personal_data.decimal_adapt_order_quantity_because_fees\n\n    def _decimal_adapt_order_quantity_because_fees(\n        exchange_manager, symbol: str, order_type: trading_enums.TraderOrderType, quantity: decimal.Decimal,\n        price: decimal.Decimal, side: trading_enums.TradeOrderSide\n    ):\n        return quantity\n\n    with mock.patch.object(\n            trading_personal_data, \"decimal_adapt_order_quantity_because_fees\",\n            mock.Mock(side_effect=_decimal_adapt_order_quantity_because_fees)\n    ) as decimal_adapt_order_quantity_because_fees_mock:\n        assert await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n        assert await consumer.create_new_orders(symbol, decimal.Decimal(str(0.5)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n        assert await consumer.create_new_orders(symbol, trading_constants.ZERO, trading_enums.EvaluatorStates.NEUTRAL.value) == []\n        assert await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.5)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n        assert await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n        # neutral state\n        decimal_adapt_order_quantity_because_fees_mock.assert_not_called()\n\n        # valid sell limit order (price adapted)\n        orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.65)), trading_enums.EvaluatorStates.SHORT.value)\n        # short state\n        decimal_adapt_order_quantity_because_fees_mock.assert_not_called()\n        assert len(orders) == 1\n        order = orders[0]\n        assert isinstance(order, trading_personal_data.SellLimitOrder)\n        assert order.currency == \"BTC\"\n        assert order.symbol == \"BTC/USDT\"\n        assert order.origin_price == decimal.Decimal(str(7062.64011187))\n        assert order.created_last_price == last_btc_price\n        assert order.order_type == trading_enums.TraderOrderType.SELL_LIMIT\n        assert order.side == trading_enums.TradeOrderSide.SELL\n        assert order.status == trading_enums.OrderStatus.OPEN\n        assert order.exchange_manager == exchange_manager\n        assert order.trader == trader\n        assert order.fee is None\n        assert order.filled_price == trading_constants.ZERO\n        assert order.origin_quantity == decimal.Decimal(str(7.6))\n        assert order.filled_quantity == trading_constants.ZERO\n        assert order.simulated is True\n        assert order.chained_orders == []\n        assert isinstance(order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n\n        trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n        trading_mode_test_toolkit.check_oco_order_group(order,\n                                                        trading_enums.TraderOrderType.STOP_LOSS, decimal.Decimal(str(6658.73524999)),\n                                                        market_status)\n\n        # valid buy limit order with (price and quantity adapted)\n        orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.65)), trading_enums.EvaluatorStates.LONG.value)\n        assert len(orders) == 1\n        order = orders[0]\n        # long state\n        adapted_args = list(decimal_adapt_order_quantity_because_fees_mock.mock_calls[0].args)\n        adapted_args[3] = trading_personal_data.decimal_adapt_quantity(market_status, adapted_args[3])\n        adapted_args[4] = trading_personal_data.decimal_adapt_price(market_status, adapted_args[4])\n        assert adapted_args == [\n            exchange_manager, order.symbol, trading_enums.TraderOrderType.BUY_LIMIT,\n            order.origin_quantity,\n            order.origin_price,\n            trading_enums.TradeOrderSide.BUY,\n        ]\n        decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n        assert isinstance(order, trading_personal_data.BuyLimitOrder)\n        assert order.currency == \"BTC\"\n        assert order.symbol == \"BTC/USDT\"\n        assert order.origin_price == decimal.Decimal(str(6955.74988812))\n        assert order.created_last_price == last_btc_price\n        assert order.order_type == trading_enums.TraderOrderType.BUY_LIMIT\n        assert order.side == trading_enums.TradeOrderSide.BUY\n        assert order.status == trading_enums.OrderStatus.OPEN\n        assert order.exchange_manager == exchange_manager\n        assert order.trader == trader\n        assert order.fee is None\n        assert order.filled_price == trading_constants.ZERO\n        assert order.origin_quantity == decimal.Decimal(str(0.12554936))\n        assert order.filled_quantity == trading_constants.ZERO\n        assert order.simulated is True\n        assert order.order_group is None\n        assert order.chained_orders == []\n\n        trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n        truncated_last_price = trading_personal_data.decimal_trunc_with_n_decimal_digits(last_btc_price, 8)\n\n        # valid buy market order with (price and quantity adapted) using user_given quantity (which is adapted as well)\n        orders = await consumer.create_new_orders(\n            symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.VERY_LONG.value,\n            data={\n                consumer.VOLUME_KEY: decimal.Decimal('0.0123')\n            }\n        )\n        assert len(orders) == 1\n        order = orders[0]\n        assert order.origin_quantity == decimal.Decimal('0.0123')\n        # very long state\n        adapted_args = list(decimal_adapt_order_quantity_because_fees_mock.mock_calls[0].args)\n        adapted_args[3] = trading_personal_data.decimal_adapt_quantity(market_status, adapted_args[3])\n        adapted_args[4] = trading_personal_data.decimal_adapt_price(market_status, adapted_args[4])\n        assert adapted_args == [\n            exchange_manager, order.symbol, trading_enums.TraderOrderType.BUY_MARKET,\n            order.origin_quantity,\n            order.origin_price,\n            trading_enums.TradeOrderSide.BUY,\n        ]\n        decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n        assert isinstance(order, trading_personal_data.BuyMarketOrder)\n        assert order.currency == \"BTC\"\n        assert order.symbol == \"BTC/USDT\"\n        assert order.origin_price == truncated_last_price\n        assert order.created_last_price == truncated_last_price\n        assert order.order_type == trading_enums.TraderOrderType.BUY_MARKET\n        assert order.side == trading_enums.TradeOrderSide.BUY\n        assert order.status == trading_enums.OrderStatus.FILLED\n        # order has been cleared\n        assert order.exchange_manager is None\n        assert order.trader is None\n        assert order.fee\n        assert order.filled_price == decimal.Decimal(str(7009.19499999))\n        assert order.origin_quantity == decimal.Decimal('0.0123')\n        assert order.filled_quantity == order.origin_quantity\n        assert order.simulated is True\n        assert order.order_group is None\n        assert order.chained_orders == []\n\n        trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n        # valid buy market order with (price and quantity adapted)\n        orders = await consumer.create_new_orders(symbol, trading_constants.ONE,\n                                                  trading_enums.EvaluatorStates.VERY_SHORT.value)\n        # very short state\n        decimal_adapt_order_quantity_because_fees_mock.assert_not_called()\n        assert len(orders) == 1\n        order = orders[0]\n        assert isinstance(order, trading_personal_data.SellMarketOrder)\n        assert order.currency == \"BTC\"\n        assert order.symbol == \"BTC/USDT\"\n        assert order.origin_price == truncated_last_price\n        assert order.created_last_price == truncated_last_price\n        assert order.order_type == trading_enums.TraderOrderType.SELL_MARKET\n        assert order.side == trading_enums.TradeOrderSide.SELL\n        assert order.status == trading_enums.OrderStatus.FILLED\n        assert order.exchange_manager is None\n        assert order.trader is None\n        assert order.fee\n        assert order.filled_price == decimal.Decimal(str(7009.19499999))\n        assert order.origin_quantity == decimal.Decimal('2.4122877')\n        assert order.filled_quantity == order.origin_quantity\n        assert order.simulated is True\n        assert order.order_group is None\n        assert order.chained_orders == []\n\n        trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n\nasync def test_valid_create_new_orders_ref_market_as_quote(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\n        symbol] = last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n\n    # portfolio: \"BTC\": 10 \"USD\": 1000\n    # order from neutral state\n    assert await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n    assert await consumer.create_new_orders(symbol, decimal.Decimal(str(0.5)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n    assert await consumer.create_new_orders(symbol, decimal.Decimal(str(0)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n    assert await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.5)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n    assert await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.NEUTRAL.value) == []\n\n    # valid sell limit order (price adapted)\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.65)), trading_enums.EvaluatorStates.SHORT.value)\n    assert len(orders) == 1\n    order = orders[0]\n    assert isinstance(order, trading_personal_data.SellLimitOrder)\n    assert order.currency == \"BTC\"\n    assert order.symbol == \"BTC/USDT\"\n    assert order.origin_price == decimal.Decimal(str(7062.64011187))\n    assert order.created_last_price == last_btc_price\n    assert order.order_type == trading_enums.TraderOrderType.SELL_LIMIT\n    assert order.side == trading_enums.TradeOrderSide.SELL\n    assert order.status == trading_enums.OrderStatus.OPEN\n    assert order.exchange_manager == exchange_manager\n    assert order.trader == trader\n    assert order.fee is None\n    assert order.filled_price == trading_constants.ZERO\n    assert order.origin_quantity == decimal.Decimal(str(4.4))\n    assert order.filled_quantity == trading_constants.ZERO\n    assert order.simulated is True\n    assert isinstance(order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n\n    market_status = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n    trading_mode_test_toolkit.check_oco_order_group(order,\n                                                    trading_enums.TraderOrderType.STOP_LOSS, decimal.Decimal(str(6658.73524999)),\n                                                    market_status)\n\n    # valid buy limit order with (price and quantity adapted)\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.65)), trading_enums.EvaluatorStates.LONG.value)\n    assert len(orders) == 1\n    order = orders[0]\n    assert isinstance(order, trading_personal_data.BuyLimitOrder)\n    assert order.currency == \"BTC\"\n    assert order.symbol == \"BTC/USDT\"\n    assert order.origin_price == decimal.Decimal(str(6955.74988812))\n    assert order.created_last_price == last_btc_price\n    assert order.order_type == trading_enums.TraderOrderType.BUY_LIMIT\n    assert order.side == trading_enums.TradeOrderSide.BUY\n    assert order.status == trading_enums.OrderStatus.OPEN\n    assert order.exchange_manager == exchange_manager\n    assert order.trader == trader\n    assert order.fee is None\n    assert order.filled_price == trading_constants.ZERO\n    assert order.origin_quantity == decimal.Decimal(str(0.21685799))\n    assert order.filled_quantity == trading_constants.ZERO\n    assert order.simulated is True\n    assert order.order_group is None\n    assert order.chained_orders == []\n\n    trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n    truncated_last_price = trading_personal_data.decimal_trunc_with_n_decimal_digits(last_btc_price, 8)\n\n    # valid buy market order with (price and quantity adapted)\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.VERY_LONG.value)\n    assert len(orders) == 1\n    order = orders[0]\n    assert isinstance(order, trading_personal_data.BuyMarketOrder)\n    assert order.currency == \"BTC\"\n    assert order.symbol == \"BTC/USDT\"\n    assert order.origin_price == truncated_last_price\n    assert order.created_last_price == truncated_last_price\n    assert order.order_type == trading_enums.TraderOrderType.BUY_MARKET\n    assert order.side == trading_enums.TradeOrderSide.BUY\n    assert order.status == trading_enums.OrderStatus.FILLED\n    assert order.filled_price == decimal.Decimal(str(7009.19499999))\n    assert order.origin_quantity == decimal.Decimal(str(0.07013502))\n    assert order.filled_quantity == order.origin_quantity\n    assert order.simulated is True\n    assert order.order_group is None\n    assert order.chained_orders == []\n\n    trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n    # valid buy market order with (price and quantity adapted)\n    orders = await consumer.create_new_orders(symbol, trading_constants.ONE, trading_enums.EvaluatorStates.VERY_SHORT.value)\n    assert len(orders) == 1\n    order = orders[0]\n    assert isinstance(order, trading_personal_data.SellMarketOrder)\n    assert order.currency == \"BTC\"\n    assert order.symbol == \"BTC/USDT\"\n    assert order.origin_price == truncated_last_price\n    assert order.created_last_price == truncated_last_price\n    assert order.order_type == trading_enums.TraderOrderType.SELL_MARKET\n    assert order.side == trading_enums.TradeOrderSide.SELL\n    assert order.status == trading_enums.OrderStatus.FILLED\n    assert order.fee\n    assert order.filled_price == decimal.Decimal(str(7009.19499999))\n    assert order.origin_quantity == decimal.Decimal(str(4.08244671))\n    assert order.filled_quantity == order.origin_quantity\n    assert order.simulated is True\n    assert order.order_group is None\n    assert order.chained_orders == []\n\n    trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n\nasync def test_invalid_create_new_orders(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # portfolio: \"BTC\": 10 \"USD\": 1000\n    min_trigger_market = \"ADA/BNB\"\n\n    # invalid sell order with not trade data\n    import octobot_trading.constants\n    trading_constants.ORDER_DATA_FETCHING_TIMEOUT = 0.1\n    assert await consumer.create_new_orders(min_trigger_market, decimal.Decimal(str(0.6)), trading_enums.EvaluatorStates.SHORT.value,\n                                            timeout=1) == []\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = decimal.Decimal(str(0.000000000000000000005))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = decimal.Decimal(str(2000))\n\n    # invalid sell order with not enough currency to sell\n    with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n        await consumer.create_new_orders(symbol, decimal.Decimal(str(0.6)), trading_enums.EvaluatorStates.SHORT.value)\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available = decimal.Decimal(str(0.000000000000000000005))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").total = decimal.Decimal(str(2000))\n\n    # invalid buy order with not enough currency to buy\n    with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n        orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.6)), trading_enums.EvaluatorStates.LONG.value)\n\n\nasync def test_create_new_orders_with_dusts_included(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = decimal.Decimal(str(0.000015))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = decimal.Decimal(str(0.000015))\n\n    # trigger order that should not sell everything but does sell everything because remaining amount\n    # is not sellable\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.6)), trading_enums.EvaluatorStates.VERY_SHORT.value)\n    assert len(orders) == 1\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = trading_constants.ZERO\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = trading_constants.ZERO\n\n    test_currency = \"NEO\"\n    test_pair = f\"{test_currency}/BTC\"\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(test_currency).available = decimal.Decimal(str(0.44))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(test_currency).total = decimal.Decimal(str(0.44))\n\n    trading_api.force_set_mark_price(exchange_manager, test_pair, 0.005318)\n    # trigger order that should not sell everything but does sell everything because remaining amount\n    # is not sellable\n    orders = await consumer.create_new_orders(test_pair, decimal.Decimal(str(0.75445456165478)),\n                                              trading_enums.EvaluatorStates.SHORT.value)\n    assert len(orders) == 1\n    assert exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(test_currency).available == trading_constants.ZERO\n    assert exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(test_currency).total == orders[0].origin_quantity\n\n\nasync def test_split_create_new_orders(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # change reference market to get more orders\n    exchange_manager.exchange_personal_data.portfolio_manager.reference_market = \"USDT\"\n    exchange_manager.exchange_personal_data.portfolio_manager.reference_market = \"USDT\"\n    market_status = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = decimal.Decimal(str(2000000001))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = decimal.Decimal(str(2000000001))\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\n        symbol] = last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(last_btc_price * 2000000001 + 1000))\n\n    # split orders because order too big and coin price too high\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.6)), trading_enums.EvaluatorStates.SHORT.value)\n    assert len(orders) == 11\n    adapted_order = orders[0]\n    identical_orders = orders[1:]\n\n    assert isinstance(adapted_order, trading_personal_data.SellLimitOrder)\n    assert adapted_order.currency == \"BTC\"\n    assert adapted_order.symbol == \"BTC/USDT\"\n    assert adapted_order.origin_price == decimal.Decimal(str(7065.26855999))\n    assert adapted_order.created_last_price == last_btc_price\n    assert adapted_order.order_type == trading_enums.TraderOrderType.SELL_LIMIT\n    assert adapted_order.side == trading_enums.TradeOrderSide.SELL\n    assert adapted_order.status == trading_enums.OrderStatus.OPEN\n    assert adapted_order.exchange_manager == exchange_manager\n    assert adapted_order.trader == trader\n    assert adapted_order.fee is None\n    assert adapted_order.filled_price == trading_constants.ZERO\n    assert adapted_order.origin_quantity == decimal.Decimal(str(64625635.97358073))\n    assert adapted_order.filled_quantity == trading_constants.ZERO\n    assert adapted_order.simulated is True\n    assert isinstance(adapted_order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n\n    trading_mode_test_toolkit.check_order_limits(adapted_order, market_status)\n\n    trading_mode_test_toolkit.check_oco_order_group(adapted_order,\n                                                    trading_enums.TraderOrderType.STOP_LOSS, decimal.Decimal(str(6658.73524999)),\n                                                    market_status)\n\n    for order in identical_orders:\n        assert isinstance(order, trading_personal_data.SellLimitOrder)\n        assert order.currency == adapted_order.currency\n        assert order.symbol == adapted_order.symbol\n        assert order.origin_price == adapted_order.origin_price\n        assert order.created_last_price == adapted_order.created_last_price\n        assert order.order_type == adapted_order.order_type\n        assert order.side == adapted_order.side\n        assert order.status == adapted_order.status\n        assert order.exchange_manager == adapted_order.exchange_manager\n        assert order.trader == adapted_order.trader\n        assert order.fee == adapted_order.fee\n        assert order.filled_price == adapted_order.filled_price\n        assert order.origin_quantity == decimal.Decimal(str(141537436.47664192))\n        assert order.origin_quantity > adapted_order.origin_quantity\n        assert order.filled_quantity == trading_constants.ZERO\n        assert order.simulated == adapted_order.simulated\n        assert isinstance(order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n\n        trading_mode_test_toolkit.check_order_limits(order, market_status)\n        trading_mode_test_toolkit.check_oco_order_group(order,\n                                                        trading_enums.TraderOrderType.STOP_LOSS, decimal.Decimal(str(6658.73524999)),\n                                                        market_status)\n\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available = decimal.Decimal(str(40000000000))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").total = decimal.Decimal(str(40000000000))\n\n    # set btc last price to 6998.55407999 * 0.000001 = 0.00699855408\n    trading_api.force_set_mark_price(exchange_manager, symbol, float(last_btc_price * decimal.Decimal(str(0.000001))))\n    # split orders because order too big and too many coins\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.6)), trading_enums.EvaluatorStates.LONG.value)\n    assert len(orders) == 3\n    adapted_order = orders[0]\n    identical_orders = orders[1:]\n\n    assert isinstance(adapted_order, trading_personal_data.BuyLimitOrder)\n    assert adapted_order.currency == \"BTC\"\n    assert adapted_order.symbol == \"BTC/USDT\"\n    assert adapted_order.origin_price == decimal.Decimal(str(0.00695312))\n    assert adapted_order.created_last_price == decimal.Decimal(str(0.007009194999999998))\n    assert adapted_order.order_type == trading_enums.TraderOrderType.BUY_LIMIT\n    assert adapted_order.side == trading_enums.TradeOrderSide.BUY\n    assert adapted_order.status == trading_enums.OrderStatus.OPEN\n    assert adapted_order.exchange_manager == exchange_manager\n    assert adapted_order.trader == trader\n    assert adapted_order.fee is None\n    assert adapted_order.filled_price == trading_constants.ZERO\n    assert adapted_order.origin_quantity == decimal.Decimal(\"396851564266.65327383\")\n    assert adapted_order.filled_quantity == trading_constants.ZERO\n    assert adapted_order.simulated is True\n\n    trading_mode_test_toolkit.check_order_limits(adapted_order, market_status)\n\n    for order in identical_orders:\n        assert isinstance(order, trading_personal_data.BuyLimitOrder)\n        assert order.currency == adapted_order.currency\n        assert order.symbol == adapted_order.symbol\n        assert order.origin_price == adapted_order.origin_price\n        assert order.created_last_price == adapted_order.created_last_price\n        assert order.order_type == adapted_order.order_type\n        assert order.side == adapted_order.side\n        assert order.status == adapted_order.status\n        assert order.exchange_manager == adapted_order.exchange_manager\n        assert order.trader == adapted_order.trader\n        assert order.fee == adapted_order.fee\n        assert order.filled_price == adapted_order.filled_price\n        assert order.origin_quantity == decimal.Decimal(str(1000000000000.0))\n        assert order.origin_quantity > adapted_order.origin_quantity\n        assert order.filled_quantity == trading_constants.ZERO\n        assert order.simulated == adapted_order.simulated\n\n        trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n\nasync def test_valid_create_new_orders_without_stop_order(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # change reference market to get more orders\n    exchange_manager.exchange_personal_data.portfolio_manager.reference_market = \"USDT\"\n    exchange_manager.exchange_personal_data.portfolio_manager.reference_market = \"USDT\"\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\n        symbol] = last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(last_btc_price * 10 + 1000))\n    market_status = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n\n    # force no stop orders\n    consumer.USE_STOP_ORDERS = False\n\n    # valid sell limit order (price adapted)\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.65)), trading_enums.EvaluatorStates.SHORT.value)\n    assert len(orders) == 1\n    order = orders[0]\n    assert isinstance(order, trading_personal_data.SellLimitOrder)\n    assert order.currency == \"BTC\"\n    assert order.symbol == \"BTC/USDT\"\n    assert order.origin_price == decimal.Decimal(str(7062.64011187))\n    assert order.created_last_price == last_btc_price\n    assert order.order_type == trading_enums.TraderOrderType.SELL_LIMIT\n    assert order.side == trading_enums.TradeOrderSide.SELL\n    assert order.status == trading_enums.OrderStatus.OPEN\n    assert order.exchange_manager == exchange_manager\n    assert order.trader == trader\n    assert order.fee is None\n    assert order.filled_price == trading_constants.ZERO\n    assert order.origin_quantity == decimal.Decimal(str(7.6))\n    assert order.filled_quantity == trading_constants.ZERO\n    assert order.simulated is True\n    assert order.order_group is None\n    assert order.chained_orders == []\n\n    trading_mode_test_toolkit.check_order_limits(order, market_status)\n\n\ndef _get_evaluations_gradient(step):\n    nb_steps = 1 / step\n    return [decimal.Decimal(str(i / nb_steps)) for i in range(int(-nb_steps), int(nb_steps + 1), 1)]\n\n\ndef _get_states_gradient_with_invald_states():\n    states = [state.value for state in trading_enums.EvaluatorStates]\n    states += [None, 1, {'toto': 1}, math.nan]\n    return states\n\n\ndef _get_irrationnal_numbers():\n    irrationals = [math.pi, math.sqrt(2), math.sqrt(3), math.sqrt(5), math.sqrt(7), math.sqrt(11), math.sqrt(73),\n                   10 / 3]\n    return [decimal.Decimal(str(1 / i)) for i in irrationals]\n\n\ndef _reset_portfolio(exchange_manager):\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = decimal.Decimal(str(10))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = decimal.Decimal(str(10))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available = decimal.Decimal(str(2000))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").total = decimal.Decimal(str(2000))\n\n\nasync def test_create_orders_using_a_lot_of_different_inputs_with_portfolio_reset(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n    gradient_step = 0.005\n    nb_orders = 1\n    initial_portfolio = copy.copy(exchange_manager.exchange_personal_data.portfolio_manager.portfolio)\n    portfolio_wrapper = exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n    market_status = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    min_trigger_market = \"ADA/BNB\"\n    trading_api.force_set_mark_price(exchange_manager, min_trigger_market, 0.001)\n\n    for state in _get_states_gradient_with_invald_states():\n        for evaluation in _get_evaluations_gradient(gradient_step):\n            _reset_portfolio(exchange_manager)\n            # orders are possible\n            try:\n                orders = await consumer.create_new_orders(symbol, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, nb_orders, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n            # orders are impossible\n            try:\n                orders = []\n                orders = await consumer.create_new_orders(min_trigger_market, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, 0, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n\n        for evaluation in _get_irrationnal_numbers():\n            # orders are possible\n            _reset_portfolio(exchange_manager)\n            try:\n                orders = await consumer.create_new_orders(symbol, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, nb_orders, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n            # orders are impossible\n            try:\n                orders = []\n                orders = await consumer.create_new_orders(min_trigger_market, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, 0, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n\n        _reset_portfolio(exchange_manager)\n        # orders are possible\n        try:\n            orders = await consumer.create_new_orders(symbol, decimal.Decimal(\"nan\"), state)\n            trading_mode_test_toolkit.check_orders(orders, decimal.Decimal(\"nan\"), state, nb_orders, market_status)\n            trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n        except trading_errors.MissingMinimalExchangeTradeVolume:\n            pass\n        # orders are impossible\n        try:\n            orders = []\n            orders = await consumer.create_new_orders(min_trigger_market, decimal.Decimal(\"nan\"), state)\n            trading_mode_test_toolkit.check_orders(orders, decimal.Decimal(\"nan\"), state, 0, market_status)\n            trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n        except trading_errors.MissingMinimalExchangeTradeVolume:\n            pass\n        try:\n            orders = []\n            # float evaluation\n            orders = await consumer.create_new_orders(min_trigger_market, math.nan, state)\n            trading_mode_test_toolkit.check_orders(orders, math.nan, state, 0, market_status)\n            trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders)\n        except trading_errors.MissingMinimalExchangeTradeVolume:\n            pass\n\n\nasync def test_create_order_using_a_lot_of_different_inputs_without_portfolio_reset(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    gradient_step = 0.001\n    nb_orders = \"unknown\"\n    initial_portfolio = copy.copy(exchange_manager.exchange_personal_data.portfolio_manager.portfolio)\n    portfolio_wrapper = exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n    market_status = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    min_trigger_market = \"ADA/BNB\"\n    trading_api.force_set_mark_price(exchange_manager, min_trigger_market, 0.001)\n\n    _reset_portfolio(exchange_manager)\n    for state in _get_states_gradient_with_invald_states():\n        for evaluation in _get_evaluations_gradient(gradient_step):\n            # orders are possible\n            try:\n                orders = await consumer.create_new_orders(symbol, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, nb_orders, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders, True)\n                await trading_mode_test_toolkit.fill_orders(orders, trader)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n            # orders are impossible\n            try:\n                orders = []\n                orders = await consumer.create_new_orders(min_trigger_market, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, 0, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders, True)\n                await trading_mode_test_toolkit.fill_orders(orders, trader)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n\n    _reset_portfolio(exchange_manager)\n    for state in _get_states_gradient_with_invald_states():\n        for evaluation in _get_irrationnal_numbers():\n            # orders are possible\n            try:\n                orders = await consumer.create_new_orders(symbol, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, nb_orders, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders, True)\n                if any(order\n                       for order in orders\n                       if order.order_type not in (\n                trading_enums.TraderOrderType.SELL_MARKET, trading_enums.TraderOrderType.BUY_MARKET)):\n                    # no need to fill market orders\n                    await trading_mode_test_toolkit.fill_orders(orders, trader)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n            # orders are impossible\n            try:\n                orders = []\n                orders = await consumer.create_new_orders(min_trigger_market, evaluation, state)\n                trading_mode_test_toolkit.check_orders(orders, evaluation, state, 0, market_status)\n                trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders, True)\n                if any(order\n                       for order in orders\n                       if order.order_type not in (\n                trading_enums.TraderOrderType.SELL_MARKET, trading_enums.TraderOrderType.BUY_MARKET)):\n                    # no need to fill market orders\n                    await trading_mode_test_toolkit.fill_orders(orders, trader)\n            except trading_errors.MissingMinimalExchangeTradeVolume:\n                pass\n\n    _reset_portfolio(exchange_manager)\n    for state in _get_states_gradient_with_invald_states():\n        # orders are possible\n        try:\n            orders = await consumer.create_new_orders(symbol, decimal.Decimal(\"nan\"), state)\n            trading_mode_test_toolkit.check_orders(orders, math.nan, state, nb_orders, market_status)\n            trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders, True)\n            await trading_mode_test_toolkit.fill_orders(orders, trader)\n        except trading_errors.MissingMinimalExchangeTradeVolume:\n            pass\n        # orders are impossible\n        try:\n            orders = []\n            orders = await consumer.create_new_orders(min_trigger_market, decimal.Decimal(\"nan\"), state)\n            trading_mode_test_toolkit.check_orders(orders, math.nan, state, 0, market_status)\n            trading_mode_test_toolkit.check_portfolio(portfolio_wrapper, initial_portfolio, orders, True)\n            await trading_mode_test_toolkit.fill_orders(orders, trader)\n        except trading_errors.MissingMinimalExchangeTradeVolume:\n            pass\n\n\nasync def test_create_multiple_buy_orders_after_fill(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n    # force many traded asset not to create all in orders\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.origin_crypto_currencies_values \\\n        = {\n        \"a\": trading_constants.ZERO,\n        \"b\": trading_constants.ZERO,\n        \"c\": trading_constants.ZERO,\n        \"d\": trading_constants.ZERO,\n        \"e\": trading_constants.ZERO\n    }\n    await ensure_smaller_orders(consumer, symbol, trader)\n\n    # with another symbol with 0 quantity when start\n    trading_api.force_set_mark_price(exchange_manager, \"ADA/BTC\", 0.0000001)\n    await ensure_smaller_orders(consumer, \"ADA/BTC\", trader)\n\n\nasync def ensure_smaller_orders(consumer, symbol, trader):\n    state = trading_enums.EvaluatorStates.VERY_LONG.value\n\n    # first call: biggest order\n    orders1 = (await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state))\n    if any(order\n           for order in orders1\n           if order.order_type not in (\n    trading_enums.TraderOrderType.SELL_MARKET, trading_enums.TraderOrderType.BUY_MARKET)):\n        # no need to fill market orders\n        await trading_mode_test_toolkit.fill_orders(orders1, trader)\n\n    state = trading_enums.EvaluatorStates.LONG.value\n    # second call: smaller order (same with very long as with long)\n    orders2 = (await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.6)), state))\n    assert orders1[0].origin_quantity > orders2[0].origin_quantity\n    if any(order\n           for order in orders2\n           if order.order_type not in (\n    trading_enums.TraderOrderType.SELL_MARKET, trading_enums.TraderOrderType.BUY_MARKET)):\n        # no need to fill market orders\n        await trading_mode_test_toolkit.fill_orders(orders2, trader)\n\n    # third call: even smaller order\n    orders3 = (await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.6)), state))\n    assert orders2[0].origin_quantity > orders3[0].origin_quantity\n    if any(order\n           for order in orders3\n           if order.order_type not in (\n    trading_enums.TraderOrderType.SELL_MARKET, trading_enums.TraderOrderType.BUY_MARKET)):\n        # no need to fill market orders\n        await trading_mode_test_toolkit.fill_orders(orders3, trader)\n\n    # third call: even-even smaller order\n    orders4 = (await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.6)), state))\n    assert orders3[0].origin_quantity > orders4[0].origin_quantity\n    if any(order\n           for order in orders4\n           if order.order_type not in (\n    trading_enums.TraderOrderType.SELL_MARKET, trading_enums.TraderOrderType.BUY_MARKET)):\n        # no need to fill market orders\n        await trading_mode_test_toolkit.fill_orders(orders4, trader)\n\n\nasync def test_create_new_orders_with_cancel_policy(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n\n    # simple buy order\n    data = {\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.CANCEL_POLICY: trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy.__name__,\n    }\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.LONG.value, data=data)\n    buy_order = orders[0]\n    assert isinstance(buy_order, trading_personal_data.BuyLimitOrder)\n    assert isinstance(buy_order.cancel_policy, trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy)\n    assert len(buy_order.chained_orders) == 0\n    \n    # buy order order with stop\n    data = {\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"10\"),\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.CANCEL_POLICY: trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy.__name__,\n    }\n    orders_with_stop = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.VERY_LONG.value, data=data)\n    buy_order = orders_with_stop[0]\n    assert isinstance(buy_order, trading_personal_data.BuyMarketOrder)\n    assert isinstance(buy_order.cancel_policy, trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy)\n    assert len(buy_order.chained_orders) == 1\n    stop_order = buy_order.chained_orders[0]\n    assert stop_order.cancel_policy is None # cancel policy is set on the entry order only\n    assert stop_order.is_open()\n\n    # simple sell order with invalid policy\n    data = {\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.CANCEL_POLICY: trading_personal_data.ExpirationTimeOrderCancelPolicy.__name__,\n    }\n    with pytest.raises(trading_errors.InvalidCancelPolicyError):\n        orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(1)), trading_enums.EvaluatorStates.SHORT.value, data=data)\n    \n    # simple sell order with valid policy\n    data = {\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.CANCEL_POLICY: trading_personal_data.ExpirationTimeOrderCancelPolicy.__name__,\n        consumer.CANCEL_POLICY_PARAMS: {\n            \"expiration_time\": 1000.0,\n        },\n    }\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(1)), trading_enums.EvaluatorStates.SHORT.value, data=data)\n    sell_order = orders[0]\n    assert isinstance(sell_order, trading_personal_data.SellLimitOrder)\n    assert isinstance(sell_order.cancel_policy, trading_personal_data.ExpirationTimeOrderCancelPolicy)\n    assert sell_order.cancel_policy.expiration_time == 1000.0\n    assert len(sell_order.chained_orders) == 0\n\nasync def test_chained_stop_loss_and_take_profit_orders(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n\n    state = trading_enums.EvaluatorStates.VERY_LONG.value\n    # stop loss only\n    data = {\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"10\"),\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.TAG_KEY: \"super\"\n    }\n    orders_with_stop = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)\n    buy_order = orders_with_stop[0]\n    assert buy_order.cancel_policy is None\n    assert len(buy_order.chained_orders) == 1\n    assert buy_order.tag == \"super\"\n    stop_order = buy_order.chained_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, stop_order.quantity_currency)\n    assert stop_order.origin_price == decimal.Decimal(\"10\")\n    # stop has been triggered as signal is triggering a buy market order that is instantly filled\n    assert stop_order.is_waiting_for_chained_trigger is False\n    assert stop_order.associated_entry_ids == [buy_order.order_id]\n    assert stop_order.tag == \"super\"\n    assert stop_order.reduce_only is False\n    assert stop_order.trailing_profile is None\n    assert stop_order.cancel_policy is None\n    assert stop_order.is_open()\n\n    state = trading_enums.EvaluatorStates.LONG.value\n    # take profit only\n    data = {\n        consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"100000\"),\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [],\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n    }\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 1\n    take_profit_order = buy_order.chained_orders[0]\n    assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n    assert take_profit_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n    assert take_profit_order.origin_price == decimal.Decimal(\"100000\")\n    assert take_profit_order.is_waiting_for_chained_trigger\n    assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n    assert take_profit_order.trailing_profile is None\n    assert not take_profit_order.is_open()\n    assert not take_profit_order.is_created()\n    assert take_profit_order.reduce_only is False\n    # take profit only using ADDITIONAL_TAKE_PROFIT_PRICES_KEY\n    data = {\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal(\"100000\")],\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n    }\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 1\n    take_profit_order = buy_order.chained_orders[0]\n    assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n    assert take_profit_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n    assert take_profit_order.origin_price == decimal.Decimal(\"100000\")\n    assert take_profit_order.is_waiting_for_chained_trigger\n    assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n    assert not take_profit_order.is_open()\n    assert not take_profit_order.is_created()\n    assert take_profit_order.reduce_only is False\n    assert take_profit_order.trailing_profile is None\n\n    # stop loss and take profit\n    data = {\n        consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"100012\"),\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"123\"),\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n    }\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state, data=data)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 2\n    stop_order = buy_order.chained_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, stop_order.quantity_currency)\n    assert stop_order.origin_price == decimal.Decimal(\"123\")\n    assert stop_order.is_waiting_for_chained_trigger\n    assert stop_order.associated_entry_ids == [buy_order.order_id]\n    assert stop_order.trailing_profile is None\n    assert stop_order.cancel_policy is None\n    assert not take_profit_order.is_open()\n    assert not take_profit_order.is_created()\n    take_profit_order = buy_order.chained_orders[1]\n    assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n    assert take_profit_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n    assert take_profit_order.origin_price == decimal.Decimal(\"100012\")\n    assert take_profit_order.is_waiting_for_chained_trigger\n    assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n    assert not take_profit_order.is_open()\n    assert not take_profit_order.is_created()\n    assert take_profit_order.reduce_only is False\n    assert isinstance(stop_order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n    assert take_profit_order.order_group is stop_order.order_group\n    assert take_profit_order.trailing_profile is None\n    assert take_profit_order.cancel_policy is None\n\n    # stop loss and take profit but decreasing position size: create stop loss and no take profit\n    # (this initial order is a take profit already)\n    state = trading_enums.EvaluatorStates.SHORT.value\n    data = {\n        consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"100012\"),\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"123\"),\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n    }\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state, data=data)\n    assert len(orders) == 1\n    sell_limit = orders[0]\n    order_group = sell_limit.order_group\n    stop_loss = exchange_manager.exchange_personal_data.orders_manager.get_order_from_group(order_group.name)[1]\n    assert isinstance(sell_limit, trading_personal_data.SellLimitOrder)\n    assert isinstance(stop_loss, trading_personal_data.StopLossOrder)\n    assert sell_limit.chained_orders == []\n    assert stop_loss.associated_entry_ids is None\n    assert stop_loss.chained_orders == []\n    assert stop_loss.reduce_only is True    # True as force stop loss\n    assert stop_loss.origin_price == decimal.Decimal(\"123\")\n    assert stop_loss.trailing_profile is None\n    assert stop_loss.cancel_policy is None\n    assert stop_loss.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(sell_limit.fee, stop_loss.quantity_currency)\n\n\nasync def test_chained_multiple_take_profit_orders(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n\n    state = trading_enums.EvaluatorStates.LONG.value\n    # 1 take profit and 2 additional (3 in total)\n    data = {\n        consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"100000\"),\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\")],\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n    }\n    orders_with_tps = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)\n    buy_order = orders_with_tps[0]\n    tp_prices = [decimal.Decimal(\"100000\"), decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\")]\n    assert len(buy_order.chained_orders) == len(tp_prices)\n    for i, take_profit_order in enumerate(buy_order.chained_orders):\n        is_last = i == len(buy_order.chained_orders) - 1\n        assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n        assert take_profit_order.origin_quantity == (\n            decimal.Decimal(\"0.01\")\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n        ) / decimal.Decimal(str(len(tp_prices)))\n        assert take_profit_order.order_group is None\n        assert take_profit_order.origin_price == tp_prices[i]\n        assert take_profit_order.is_waiting_for_chained_trigger\n        assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n        assert not take_profit_order.is_open()\n        assert not take_profit_order.is_created()\n        assert take_profit_order.update_with_triggering_order_fees == is_last\n        assert take_profit_order.trailing_profile is None\n        assert take_profit_order.is_active is True\n\n    # only 2 additional (2 in total)\n    data = {\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\")],\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"), consumer.TRAILING_PROFILE: None\n    }\n    orders_with_tps = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)\n    buy_order = orders_with_tps[0]\n    tp_prices = [decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\")]\n    assert len(buy_order.chained_orders) == len(tp_prices)\n    for i, take_profit_order in enumerate(buy_order.chained_orders):\n        is_last = i == len(buy_order.chained_orders) - 1\n        assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n        assert take_profit_order.origin_quantity == (\n            decimal.Decimal(\"0.01\")\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n        ) / decimal.Decimal(str(len(tp_prices)))\n        assert take_profit_order.origin_price == tp_prices[i]\n        assert take_profit_order.is_waiting_for_chained_trigger\n        assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n        assert not take_profit_order.is_open()\n        assert not take_profit_order.is_created()\n        assert take_profit_order.update_with_triggering_order_fees == is_last\n        assert take_profit_order.trailing_profile is None\n        assert take_profit_order.is_active is True\n\n    # only 2 additional with volume (2 in total)\n    volume_ratios = [decimal.Decimal(\"1\"), decimal.Decimal(\"1.2\")]\n    data = {\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\")],\n        consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: volume_ratios,\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"), consumer.TRAILING_PROFILE: None\n    }\n    orders_with_tps = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)\n    buy_order = orders_with_tps[0]\n    tp_prices = [decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\")]\n    assert len(buy_order.chained_orders) == len(tp_prices)\n    for i, take_profit_order in enumerate(buy_order.chained_orders):\n        is_last = i == len(buy_order.chained_orders) - 1\n        assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n        assert take_profit_order.origin_quantity == (\n            decimal.Decimal(\"0.01\")\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n        ) * volume_ratios[i] / sum(volume_ratios)\n        assert take_profit_order.origin_price == tp_prices[i]\n        assert take_profit_order.is_waiting_for_chained_trigger\n        assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n        assert not take_profit_order.is_open()\n        assert not take_profit_order.is_created()\n        assert take_profit_order.update_with_triggering_order_fees == is_last\n        assert take_profit_order.trailing_profile is None\n        assert take_profit_order.is_active is True\n\n    # stop loss and 1 take profit and 5 additional with volume data (6 TP in total)\n    exchange_manager.trader.enable_inactive_orders = True\n    tp_prices = [\n        decimal.Decimal(\"100012\"),\n        decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\"), decimal.Decimal(\"130000\"),\n        decimal.Decimal(\"140000\"), decimal.Decimal(\"150000\")\n    ]\n    tp_volumes = [\n        decimal.Decimal(str(val))\n        for val in (\n            1,\n            2, 2.5, 2,\n            3, 2\n        )\n    ]\n    data = {\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"123\"),\n        consumer.TAKE_PROFIT_PRICE_KEY: tp_prices[0],\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: tp_prices[1:],\n        consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: tp_volumes,  # inclue volume of 1st TP\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n    }\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state, data=data)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 1 + len(tp_prices)\n    stop_order = buy_order.chained_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, stop_order.quantity_currency)\n    assert stop_order.origin_price == decimal.Decimal(\"123\")\n    assert stop_order.is_waiting_for_chained_trigger\n    assert stop_order.associated_entry_ids == [buy_order.order_id]\n    assert stop_order.update_with_triggering_order_fees is True\n    assert stop_order.trailing_profile is None\n    assert stop_order.is_active is True\n    assert len(buy_order.chained_orders[1:]) == len(tp_prices)\n    for i, take_profit_order in enumerate(buy_order.chained_orders[1:]):\n        is_last = i == len(buy_order.chained_orders[1:]) - 1\n        assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n        assert take_profit_order.origin_quantity == (\n            decimal.Decimal(\"0.01\")\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n        ) * tp_volumes[i] / sum(tp_volumes)\n        assert take_profit_order.origin_price == tp_prices[i]\n        assert take_profit_order.is_active is False\n        assert take_profit_order.is_waiting_for_chained_trigger\n        assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n        assert not take_profit_order.is_open()\n        assert not take_profit_order.is_created()\n        assert isinstance(stop_order.order_group, trading_personal_data.BalancedTakeProfitAndStopOrderGroup)\n        assert take_profit_order.order_group is stop_order.order_group\n        assert take_profit_order.update_with_triggering_order_fees == is_last\n        assert take_profit_order.trailing_profile is None\n\n\nasync def test_chained_multiple_take_profit_with_filled_tp_trailing_stop_orders(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n\n    exchange_manager.trader.enable_inactive_orders = True\n    state = trading_enums.EvaluatorStates.LONG.value\n    # stop loss and 1 take profit and 5 additional (6 TP in total)\n    tp_prices = [\n        decimal.Decimal(\"100012\"),\n        decimal.Decimal(\"110000\"), decimal.Decimal(\"120000\"), decimal.Decimal(\"130000\"),\n        decimal.Decimal(\"140000\"), decimal.Decimal(\"150000\")\n    ]\n    data = {\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"123\"),\n        consumer.TAKE_PROFIT_PRICE_KEY: tp_prices[0],\n        consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: tp_prices[1:],\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.TRAILING_PROFILE: trading_personal_data.TrailingProfileTypes.FILLED_TAKE_PROFIT.value,\n    }\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state, data=data)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 1 + len(tp_prices)\n    stop_order = buy_order.chained_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.origin_quantity == decimal.Decimal(\"0.01\") \\\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, stop_order.quantity_currency)\n    assert stop_order.origin_price == decimal.Decimal(\"123\")\n    assert stop_order.is_waiting_for_chained_trigger\n    assert stop_order.associated_entry_ids == [buy_order.order_id]\n    assert stop_order.update_with_triggering_order_fees is True\n    assert stop_order.is_active is True\n    assert stop_order.trailing_profile == trading_personal_data.FilledTakeProfitTrailingProfile([\n        trading_personal_data.TrailingPriceStep(float(trailing_price), float(trigger_price), True)\n        for trailing_price, trigger_price in zip([buy_order.origin_price] + tp_prices[:-1], tp_prices)\n    ])\n    assert len(buy_order.chained_orders[1:]) == len(tp_prices)\n    for i, take_profit_order in enumerate(buy_order.chained_orders[1:]):\n        is_last = i == len(buy_order.chained_orders[1:]) - 1\n        assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n        assert take_profit_order.origin_quantity == (\n            decimal.Decimal(\"0.01\")\n           - trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)\n        ) / decimal.Decimal(str(len(tp_prices)))\n        assert take_profit_order.origin_price == tp_prices[i]\n        assert take_profit_order.is_waiting_for_chained_trigger\n        assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n        assert take_profit_order.is_active is False\n        assert not take_profit_order.is_open()\n        assert not take_profit_order.is_created()\n        assert isinstance(stop_order.order_group, trading_personal_data.TrailingOnFilledTPBalancedOrderGroup)\n        assert take_profit_order.order_group is stop_order.order_group\n        assert take_profit_order.update_with_triggering_order_fees == is_last\n        assert take_profit_order.trailing_profile is None\n\n\nasync def test_create_stop_loss_orders(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n    exchange_manager.trader.enable_inactive_orders = True\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n\n    state = trading_enums.EvaluatorStates.SHORT.value\n    data = {\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"10\"),\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.STOP_ONLY: True\n    }\n    created_orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.6)), state, data=data)\n    assert len(created_orders) == 1\n    stop_order = created_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.origin_quantity == decimal.Decimal(\"0.01\")\n    assert stop_order.origin_price == decimal.Decimal(\"10\")\n    assert stop_order.side is trading_enums.TradeOrderSide.SELL\n    assert stop_order.is_waiting_for_chained_trigger is False\n    assert stop_order.update_with_triggering_order_fees is False    # not chained order\n    assert stop_order.tag is None\n    assert stop_order.is_active is True\n    assert stop_order.is_open()\n\n    state = trading_enums.EvaluatorStates.LONG.value\n    data = {\n        consumer.STOP_PRICE_KEY: decimal.Decimal(\"5\"),\n        consumer.VOLUME_KEY: decimal.Decimal(\"0.01\"),\n        consumer.STOP_ONLY: True,\n        consumer.TAG_KEY: \"plop1\"\n    }\n    created_orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-0.6)), state, data=data)\n    assert len(created_orders) == 1\n    stop_order = created_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.origin_quantity == decimal.Decimal(\"0.01\")\n    assert stop_order.origin_price == decimal.Decimal(\"5\")\n    assert stop_order.side is trading_enums.TradeOrderSide.BUY\n    assert stop_order.is_waiting_for_chained_trigger is False\n    assert stop_order.tag == \"plop1\"\n    assert stop_order.is_active is True\n    assert stop_order.is_open()\n\n\nasync def test_get_limit_quantity_from_risk(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n    ctx = script_keywords.get_base_context(consumer.trading_mode, symbol)\n    last_btc_price = 100\n    trading_api.force_set_mark_price(exchange_manager, symbol, last_btc_price)\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\n        symbol] = last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(last_btc_price * 10 + 1000))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.current_crypto_currencies_values[\"BTC\"] = \\\n        decimal.Decimal(str(last_btc_price))\n    # with user amount\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(1)\n    consumer.trading_mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = 10\n    consumer.trading_mode.trading_config[trading_constants.CONFIG_SELL_ORDER_AMOUNT] = 10\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"10\")\n\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.5\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"9.9\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"1.9\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"1.9\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    # decreasing position\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, False) == decimal.Decimal(\"10\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, False) == decimal.Decimal(\"10\")\n\n    # without user amount\n    consumer.trading_mode.trading_config.pop(trading_constants.CONFIG_BUY_ORDER_AMOUNT)\n    consumer.trading_mode.trading_config.pop(trading_constants.CONFIG_SELL_ORDER_AMOUNT)\n\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.5\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"8.7\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"1.9\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"1.9\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    # decreasing position\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, False) == decimal.Decimal(\"15\")\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, False) == decimal.Decimal(\"15\")\n\n    # all-in orders\n    # 1. sell\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"1.9\")\n    consumer.SELL_WITH_MAXIMUM_SIZE_ORDERS = True\n    # increasing position (would be 1.9 without all-in)\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"15\")\n    # decreasing position\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, False) == decimal.Decimal(\"15\")\n\n    # 2. buy\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"1.9\")\n    consumer.BUY_WITH_MAXIMUM_SIZE_ORDERS = True\n    # increasing position (would be 1.9 without all-in)\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"15\")\n    # decreasing position\n    assert await consumer._get_limit_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, False) == decimal.Decimal(\"15\")\n\n\nasync def test_get_market_quantity_from_risk(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n    ctx = script_keywords.get_base_context(consumer.trading_mode, symbol)\n    last_btc_price = 80\n    trading_api.force_set_mark_price(exchange_manager, symbol, last_btc_price)\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[\n        symbol] = last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(last_btc_price * 10 + 1000))\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.current_crypto_currencies_values[\"BTC\"] = \\\n        decimal.Decimal(str(last_btc_price))\n    # with user amount\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(1)\n    consumer.trading_mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = 10\n    consumer.trading_mode.trading_config[trading_constants.CONFIG_SELL_ORDER_AMOUNT] = 10\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"10\")\n\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.5\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"10\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"2.125\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"2.125\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    # decreasing position\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, False) == decimal.Decimal(\"10\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, False) == decimal.Decimal(\"10\")\n\n    # without user amount\n    consumer.trading_mode.trading_config.pop(trading_constants.CONFIG_BUY_ORDER_AMOUNT)\n    consumer.trading_mode.trading_config.pop(trading_constants.CONFIG_SELL_ORDER_AMOUNT)\n\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.5\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"11.125\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"2.125\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"2.125\")\n    consumer.MAX_CURRENCY_RATIO = decimal.Decimal(\"0.1\")\n    # decreasing position\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, False) == decimal.Decimal(\"10.8\")\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, False) == decimal.Decimal(\"10.8\")\n\n    # all-in orders\n    # 1. sell\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"2.125\")\n    consumer.SELL_WITH_MAXIMUM_SIZE_ORDERS = True\n    # increasing position (would be 2.125 without all-in)\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, True) == decimal.Decimal(\"15\")\n    # decreasing position\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", True, False) == decimal.Decimal(\"15\")\n\n    # 2. buy\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"2.125\")\n    consumer.BUY_WITH_MAXIMUM_SIZE_ORDERS = True\n    # increasing position (would be 1.9 without all-in)\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, True) == decimal.Decimal(\"15\")\n    # decreasing position\n    assert await consumer._get_market_quantity_from_risk(ctx, 1, decimal.Decimal(15), \"BTC\", False, False) == decimal.Decimal(\"15\")\n\n\nasync def test_target_profit_mode(tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n    consumer.USE_TARGET_PROFIT_MODE = True\n    _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(\n        exchange_manager, symbol=symbol, timeout=1\n    )\n    state = trading_enums.EvaluatorStates.LONG.value\n    # take profit only\n    consumer.USE_STOP_ORDERS = False\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 1\n    take_profit_order = buy_order.chained_orders[0]\n    assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n    assert take_profit_order.side is trading_enums.TradeOrderSide.SELL\n    assert take_profit_order.origin_quantity == buy_order.origin_quantity\n    assert take_profit_order.reduce_only is False\n    assert take_profit_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        buy_order.origin_price * (trading_constants.ONE + consumer.TARGET_PROFIT_TAKE_PROFIT)\n    )\n    assert take_profit_order.is_waiting_for_chained_trigger\n    assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n    assert not take_profit_order.is_open()\n    assert not take_profit_order.is_created()\n\n    exchange_manager.trader.enable_inactive_orders = True\n    # stop loss and take profit\n    consumer.USE_STOP_ORDERS = True\n    orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state)\n    buy_order = orders_with_tp[0]\n    assert len(buy_order.chained_orders) == 2\n    stop_order = buy_order.chained_orders[0]\n    assert isinstance(stop_order, trading_personal_data.StopLossOrder)\n    assert stop_order.side is trading_enums.TradeOrderSide.SELL\n    assert stop_order.origin_quantity == buy_order.origin_quantity\n    assert stop_order.reduce_only is False\n    assert stop_order.is_active is True\n    assert stop_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        buy_order.origin_price * (trading_constants.ONE - consumer.TARGET_PROFIT_STOP_LOSS)\n    )\n    assert stop_order.is_waiting_for_chained_trigger\n    assert stop_order.associated_entry_ids == [buy_order.order_id]\n    assert not stop_order.is_open()\n    assert not stop_order.is_created()\n    take_profit_order = buy_order.chained_orders[1]\n    assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n    assert take_profit_order.side is trading_enums.TradeOrderSide.SELL\n    assert take_profit_order.origin_quantity == buy_order.origin_quantity\n    assert take_profit_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        buy_order.origin_price * (trading_constants.ONE + consumer.TARGET_PROFIT_TAKE_PROFIT)\n    )\n    assert take_profit_order.is_waiting_for_chained_trigger\n    assert take_profit_order.associated_entry_ids == [buy_order.order_id]\n    assert not take_profit_order.is_open()\n    assert not take_profit_order.is_created()\n    assert take_profit_order.is_active is False\n    assert isinstance(stop_order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n    assert take_profit_order.order_group is stop_order.order_group\n\n    # stop loss and take profit but decreasing position size: do nothing in this mode\n    # (this initial order is a take profit already)\n    state = trading_enums.EvaluatorStates.SHORT.value\n    orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state)\n    assert orders == []\n\n\nasync def test_target_profit_mode_futures_trading(future_tools):\n    exchange_manager, trader, symbol, consumer, last_btc_price = future_tools\n\n    # with BTC/USDT\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \\\n        last_btc_price\n    exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n        decimal.Decimal(str(10 + 1000 / last_btc_price))\n    consumer.USE_TARGET_PROFIT_MODE = True\n    consumer.TARGET_PROFIT_ENABLE_POSITION_INCREASE = True\n    _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(\n        exchange_manager, symbol=symbol, timeout=1\n    )\n\n    exchange_manager.trader.enable_inactive_orders = True\n    # take profit and stop loss / long signal\n    consumer.TARGET_PROFIT_TAKE_PROFIT = decimal.Decimal(str(10))\n    consumer.TARGET_PROFIT_STOP_LOSS = decimal.Decimal(str(2.5))\n    consumer.USE_STOP_ORDERS = True\n    long_orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), trading_enums.EvaluatorStates.LONG.value)\n    buy_order = long_orders[0]\n    assert isinstance(buy_order, trading_personal_data.BuyLimitOrder)\n    assert len(buy_order.chained_orders) == 2\n    take_profit_order = buy_order.chained_orders[1]\n    stop_loss_order = buy_order.chained_orders[0]\n    assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)\n    assert isinstance(stop_loss_order, trading_personal_data.StopLossOrder)\n    # both are active on futures\n    assert stop_loss_order.is_active is True\n    assert take_profit_order.is_active is True\n    assert take_profit_order.side is trading_enums.TradeOrderSide.SELL\n    assert take_profit_order.origin_quantity == buy_order.origin_quantity\n    assert take_profit_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        buy_order.origin_price * (trading_constants.ONE + consumer.TARGET_PROFIT_TAKE_PROFIT)\n    )\n    assert stop_loss_order.side is trading_enums.TradeOrderSide.SELL\n    assert stop_loss_order.origin_quantity == buy_order.origin_quantity\n    assert stop_loss_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        buy_order.origin_price * (trading_constants.ONE - consumer.TARGET_PROFIT_STOP_LOSS)\n    )\n\n    consumer.trading_mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"100q\"\n    # take profit and stop loss / short signal\n    short_orders = await consumer.create_new_orders(symbol, decimal.Decimal(str(1)), trading_enums.EvaluatorStates.SHORT.value)\n    sell_order = short_orders[0]\n    assert sell_order.origin_quantity == decimal.Decimal('0.01426697')  # 0.01739031 without 100q config\n    assert isinstance(sell_order, trading_personal_data.SellLimitOrder)\n    assert len(sell_order.chained_orders) == 2\n    take_profit_order = sell_order.chained_orders[1]\n    stop_loss_order = sell_order.chained_orders[0]\n    assert isinstance(take_profit_order, trading_personal_data.BuyLimitOrder)\n    assert isinstance(stop_loss_order, trading_personal_data.StopLossOrder)\n    assert take_profit_order.side is trading_enums.TradeOrderSide.BUY\n    assert take_profit_order.origin_quantity == sell_order.origin_quantity\n    assert take_profit_order.reduce_only is True\n    assert take_profit_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        sell_order.origin_price * (trading_constants.ONE - consumer.TARGET_PROFIT_TAKE_PROFIT)\n    )\n    assert stop_loss_order.side is trading_enums.TradeOrderSide.BUY\n    assert stop_loss_order.origin_quantity == sell_order.origin_quantity\n    assert stop_loss_order.reduce_only is True\n    assert stop_loss_order.origin_price == trading_personal_data.decimal_adapt_price(\n        symbol_market,\n        sell_order.origin_price * (trading_constants.ONE + consumer.TARGET_PROFIT_STOP_LOSS)\n    )\n    current_position = exchange_manager.exchange_personal_data.positions_manager \\\n        .get_symbol_position(\n        symbol,\n        trading_enums.PositionSide.BOTH\n    )\n    assert current_position.is_idle()\n    await sell_order.on_fill(force_fill=True)\n    assert not current_position.is_idle()\n    short_orders_2 = await consumer.create_new_orders(symbol, decimal.Decimal(str(1)), trading_enums.EvaluatorStates.SHORT.value)\n    # created order\n    assert len(short_orders_2) == 1\n\n    consumer.TARGET_PROFIT_ENABLE_POSITION_INCREASE = False\n    short_orders_2 = await consumer.create_new_orders(symbol, decimal.Decimal(str(1)), trading_enums.EvaluatorStates.SHORT.value)\n    # did not create order as increasing position is disabled\n    assert short_orders_2 == []\n"
  },
  {
    "path": "Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_producer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport mock\nimport pytest\nimport os\nimport os.path\nimport asyncio\nimport pytest_asyncio\n\nimport async_channel.util as channel_util\nimport octobot_backtesting.api as backtesting_api\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.tests.test_config as test_config\nimport octobot_evaluators.api as evaluators_api\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.signals as trading_signals\nimport tentacles.Trading.Mode as Mode\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def tools(symbol=\"BTC/USDT\"):\n    tentacles_manager_api.reload_tentacle_info()\n    trader = None\n    try:\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 1000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        mode = Mode.DailyTradingMode(config, exchange_manager)\n        await mode.initialize()\n        # add mode to exchange manager so that it can be stopped and freed from memory\n        exchange_manager.trading_modes.append(mode)\n        mode.get_trading_mode_consumers()[0].MAX_CURRENCY_RATIO = 1\n\n        # set BTC/USDT price at 1000 USDT\n        trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n        yield mode.producers[0], mode.get_trading_mode_consumers()[0], trader\n    finally:\n        if trader:\n            await _stop(trader)\n\n\nasync def _stop(trader):\n    for importer in backtesting_api.get_importers(trader.exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await trader.exchange_manager.exchange.backtesting.stop()\n    await trader.exchange_manager.stop()\n\n\nasync def test_default_values(tools):\n    producer, _, trader = tools\n    assert producer.state is None\n\n\nasync def test_set_state(tools):\n    currency = \"BTC\"\n    symbol = \"BTC/USDT\"\n    time_frame = \"1h\"\n    producer, consumer, trader = tools\n\n    with mock.patch.object(\n        consumer.trading_mode, \"create_order\",\n        mock.AsyncMock(wraps=consumer.trading_mode.create_order)\n    ) as create_order_mock:\n        producer.final_eval = trading_constants.ZERO\n        await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.NEUTRAL)\n        assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        # create as task to allow creator's queue to get processed\n        await asyncio.create_task(_check_open_orders_count(trader, 0))\n        create_order_mock.assert_not_called()\n\n        producer.final_eval = decimal.Decimal(-1)\n        await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.VERY_LONG)\n        assert producer.state == trading_enums.EvaluatorStates.VERY_LONG\n        _check_trades_count(trader, 0)\n        # market order got filled\n        await asyncio.create_task(_check_open_orders_count(trader, 0))\n        _check_trades_count(trader, 1)\n        create_order_mock.assert_called_once()\n        assert create_order_mock.mock_calls[0].kwargs[\"dependencies\"] == None\n        create_order_mock.reset_mock()\n\n        producer.final_eval = trading_constants.ZERO\n        await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.NEUTRAL)\n        # create as task to allow creator's queue to get processed\n        await asyncio.create_task(_check_open_orders_count(trader, 0))\n        create_order_mock.assert_not_called()\n\n        producer.final_eval = trading_constants.ONE\n        await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.VERY_SHORT)\n        assert producer.state == trading_enums.EvaluatorStates.VERY_SHORT\n        # create as task to allow creator's queue to get processed\n        await asyncio.create_task(_check_open_orders_count(trader, 0))\n        # market order was created\n        create_order_mock.assert_called_once()\n        assert create_order_mock.mock_calls[0].kwargs[\"dependencies\"] == None\n        create_order_mock.reset_mock()\n        # market order got filled\n        _check_trades_count(trader, 2)\n\n        producer.final_eval = trading_constants.ZERO\n        await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.NEUTRAL)\n        assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        # create as task to allow creator's queue to get processed\n        await asyncio.create_task(_check_open_orders_count(trader, 0))\n        create_order_mock.assert_not_called()\n\n        async def _cancel_symbol_open_orders(*args, **kwargs):\n            await origin_cancel_symbol_open_orders(*args, **kwargs)\n            return (\n                True, \n                trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"456-cancel_symbol_open_orders\")])\n            )\n\n        async def _apply_cancel_policies(*args, **kwargs):\n            await origin_apply_cancel_policies(*args, **kwargs)\n            return (\n                True, \n                trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456-cancel_policy\")])\n            )\n\n        origin_cancel_symbol_open_orders = producer.cancel_symbol_open_orders\n        origin_apply_cancel_policies = producer.apply_cancel_policies\n        producer.final_eval = decimal.Decimal(str(-0.5))\n        with mock.patch.object(\n            producer, \"cancel_symbol_open_orders\",\n            mock.AsyncMock(side_effect=_cancel_symbol_open_orders)\n        ) as cancel_symbol_open_orders_mock, mock.patch.object(\n            producer, \"apply_cancel_policies\",\n            mock.AsyncMock(side_effect=_apply_cancel_policies)\n        ) as apply_cancel_policies_mock:\n            await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.LONG)\n            cancel_symbol_open_orders_mock.assert_called_once_with(symbol)\n            cancel_symbol_open_orders_mock.reset_mock()\n            apply_cancel_policies_mock.assert_called_once_with()\n            apply_cancel_policies_mock.reset_mock()\n            assert producer.state == trading_enums.EvaluatorStates.LONG\n            # create as task to allow creator's queue to get processed\n            await asyncio.create_task(_check_open_orders_count(trader, 1))\n            create_order_mock.assert_called_once()\n            # cancelled orders dependencies are forwarded to create_order\n            expected_dependencies = trading_signals.get_orders_dependencies(\n                [mock.Mock(order_id=\"456-cancel_policy\"), mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"456-cancel_symbol_open_orders\")]\n            )\n            assert create_order_mock.mock_calls[0].kwargs[\"dependencies\"] == expected_dependencies\n            create_order_mock.reset_mock()\n\n            producer.final_eval = trading_constants.ZERO\n            await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.NEUTRAL)\n            cancel_symbol_open_orders_mock.assert_not_called()\n            apply_cancel_policies_mock.assert_not_called()\n            assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n            # create as task to allow creator's queue to get processed\n            await asyncio.create_task(_check_open_orders_count(trader, 1))\n            create_order_mock.assert_not_called()\n\n            producer.final_eval = decimal.Decimal(str(0.5))\n            await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.SHORT)\n            apply_cancel_policies_mock.assert_called_once_with()\n            apply_cancel_policies_mock.reset_mock()\n            cancel_symbol_open_orders_mock.assert_called_once_with(symbol)\n            cancel_symbol_open_orders_mock.reset_mock()\n            assert producer.state == trading_enums.EvaluatorStates.SHORT\n            # let both other be created\n            await asyncio_tools.wait_asyncio_next_cycle()\n            # create as task to allow creator's queue to get processed\n            await asyncio.create_task(_check_open_orders_count(trader, 2))  # has stop loss\n            assert create_order_mock.call_count == 2\n            # cancelled orders dependencies are forwarded to all created orders\n            assert create_order_mock.mock_calls[0].kwargs[\"dependencies\"] == expected_dependencies\n            assert create_order_mock.mock_calls[1].kwargs[\"dependencies\"] == expected_dependencies\n            create_order_mock.reset_mock()\n\n            producer.final_eval = trading_constants.ZERO\n            await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.NEUTRAL)\n            cancel_symbol_open_orders_mock.assert_not_called()\n            apply_cancel_policies_mock.assert_not_called()\n            assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n            # create as task to allow creator's queue to get processed\n            await asyncio.create_task(_check_open_orders_count(trader, 2))\n            create_order_mock.assert_not_called()\n\n\nasync def test_get_delta_risk(tools):\n    producer, consumer, trader = tools\n    for i in range(0, 100, 1):\n        trader.risk = decimal.Decimal(str(i / 100))\n        assert round(producer._get_delta_risk(), 6) \\\n               == round(decimal.Decimal(str(producer.RISK_THRESHOLD * i / 100)), 6)\n\n\nasync def test_create_state(tools):\n    producer, consumer, trader = tools\n    delta_risk = producer._get_delta_risk()\n    for i in range(-100, 100, 1):\n        producer.final_eval = decimal.Decimal(str(i / 100))\n        await producer.create_state(None, None)\n        if producer.final_eval < producer.VERY_LONG_THRESHOLD + delta_risk:\n            assert producer.state == trading_enums.EvaluatorStates.VERY_LONG\n        elif producer.final_eval < producer.LONG_THRESHOLD + delta_risk:\n            assert producer.state == trading_enums.EvaluatorStates.LONG\n        elif producer.final_eval < producer.NEUTRAL_THRESHOLD - delta_risk:\n            assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        elif producer.final_eval < producer.SHORT_THRESHOLD - delta_risk:\n            assert producer.state == trading_enums.EvaluatorStates.SHORT\n        else:\n            assert producer.state == trading_enums.EvaluatorStates.VERY_SHORT\n\n\nasync def test_set_final_eval(tools):\n    currency = \"BTC\"\n    symbol = \"BTC/USDT\"\n    time_frame = \"1h\"\n    producer, consumer, trader = tools\n    matrix_id = evaluators_api.create_matrix()\n\n    await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.SHORT)\n    assert producer.state == trading_enums.EvaluatorStates.SHORT\n    # let both other be created\n    await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, 2))  # has stop loss\n    producer.final_eval = \"val\"\n    await producer.set_final_eval(matrix_id, currency, symbol, time_frame,\n                                  commons_enums.TriggerSource.EVALUATION_MATRIX.value)\n    assert producer.state == trading_enums.EvaluatorStates.SHORT  # ensure did not change trading_enums.EvaluatorStates\n    assert producer.final_eval == \"val\"  # ensure did not change trading_enums.EvaluatorStates\n    await asyncio.create_task(_check_open_orders_count(trader, 2))  # ensure did not change orders\n\n\nasync def test_finalize(tools):\n    currency = \"BTC\"\n    symbol = \"BTC/USDT\"\n    producer, consumer, trader = tools\n    matrix_id = evaluators_api.create_matrix()\n\n    await producer.finalize(trading_api.get_exchange_name(trader.exchange_manager), matrix_id, currency, symbol)\n    assert producer.final_eval == trading_constants.ZERO\n\n    await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.SHORT)\n    assert producer.state == trading_enums.EvaluatorStates.SHORT\n    # let both other be created\n    await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, 2))  # has stop loss\n\n    await producer._set_state(currency, symbol, trading_enums.EvaluatorStates.SHORT)\n    await asyncio.create_task(\n        _check_open_orders_count(trader, 2))  # ensure did not change orders because neutral state\n\n\nasync def _check_open_orders_count(trader, count):\n    assert len(trading_api.get_open_orders(trader.exchange_manager)) == count\n\n\ndef _check_trades_count(trader, count):\n    assert len(trading_api.get_trade_history(trader.exchange_manager)) == count\n"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/__init__.py",
    "content": "from .dca_trading import DCATradingMode"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/config/DCATradingMode.json",
    "content": "{\n    \"default_config\": [\n        \"SimpleStrategyEvaluator\"\n    ],\n    \"required_strategies\": [\n        \"SimpleStrategyEvaluator\"\n    ],\n    \"buy_order_amount\": \"50q\",\n    \"exit_limit_orders_price_percent\": 5,\n    \"minutes_before_next_buy\": 10080,\n    \"trigger_mode\": \"Time based\",\n    \"use_market_entry_orders\": true,\n    \"use_secondary_entry_orders\": false,\n    \"use_secondary_exit_orders\": false,\n    \"use_stop_losses\": false,\n    \"use_take_profit_exit_orders\": false,\n    \"cancel_open_orders_at_each_entry\": true,\n    \"enable_health_check\": false,\n    \"health_check_orphan_funds_threshold\": 15,\n    \"max_asset_holding_percent\": 100\n}"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/dca_trading.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport decimal\nimport enum\nimport typing\n\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.evaluators_util as evaluators_util\nimport octobot_commons.signals as commons_signals\n\nimport octobot_evaluators.api as evaluators_api\nimport octobot_evaluators.constants as evaluators_constants\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_evaluators.matrix as matrix\n\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.util as trading_util\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.exchanges as trading_exchanges\nimport octobot_trading.modes.script_keywords as script_keywords\n\n\nclass TriggerMode(enum.Enum):\n    TIME_BASED = \"Time based\"\n    MAXIMUM_EVALUATORS_SIGNALS_BASED = \"Maximum evaluators signals based\"\n\n\nclass DCATradingModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    AMOUNT_TO_BUY_IN_REF_MARKET = \"amount_to_buy_in_reference_market\"\n    ENTRY_LIMIT_ORDERS_PRICE_PERCENT = \"entry_limit_orders_price_percent\"\n    USE_MARKET_ENTRY_ORDERS = \"use_market_entry_orders\"\n    USE_INIT_ENTRY_ORDERS = \"use_init_entry_orders\"\n    USE_SECONDARY_ENTRY_ORDERS = \"use_secondary_entry_orders\"\n    SECONDARY_ENTRY_ORDERS_COUNT = \"secondary_entry_orders_count\"\n    SECONDARY_ENTRY_ORDERS_AMOUNT = \"secondary_entry_orders_amount\"\n    SECONDARY_ENTRY_ORDERS_PRICE_PERCENT = \"secondary_entry_orders_price_percent\"\n    DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER = decimal.Decimal(\"0.05\")  # 5% by default\n    DEFAULT_SECONDARY_ENTRY_ORDERS_COUNT = 0\n    DEFAULT_SECONDARY_ENTRY_ORDERS_AMOUNT = \"\"\n    DEFAULT_SECONDARY_ENTRY_ORDERS_PRICE_MULTIPLIER = DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n\n    USE_TAKE_PROFIT_EXIT_ORDERS = \"use_take_profit_exit_orders\"\n    EXIT_LIMIT_ORDERS_PRICE_PERCENT = \"exit_limit_orders_price_percent\"\n    DEFAULT_EXIT_LIMIT_PRICE_MULTIPLIER = DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n    USE_SECONDARY_EXIT_ORDERS = \"use_secondary_exit_orders\"\n    SECONDARY_EXIT_ORDERS_COUNT = \"secondary_exit_orders_count\"\n    SECONDARY_EXIT_ORDERS_PRICE_PERCENT = \"secondary_exit_orders_price_percent\"\n    DEFAULT_SECONDARY_EXIT_ORDERS_COUNT = 0\n    DEFAULT_SECONDARY_EXIT_ORDERS_PRICE_MULTIPLIER = DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n\n    USE_STOP_LOSSES = \"use_stop_losses\"\n    STOP_LOSS_PRICE_PERCENT = \"stop_loss_price_percent\"\n    DEFAULT_STOP_LOSS_ORDERS_PRICE_MULTIPLIER = 2 * DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n\n    async def create_new_orders(self, symbol, _, state, **kwargs):\n        current_order = None\n        initial_dependencies = kwargs.get(self.CREATE_ORDER_DEPENDENCIES_PARAM, None)\n        post_cancel_dependencies = None\n        try:\n            price = await trading_personal_data.get_up_to_date_price(\n                self.exchange_manager, symbol, timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT\n            )\n            symbol_market = self.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n            created_orders = []\n            ctx = script_keywords.get_base_context(self.trading_mode, symbol)\n            if state is trading_enums.EvaluatorStates.NEUTRAL.value:\n                raise trading_errors.NotSupported(state)\n            side = trading_enums.TradeOrderSide.BUY if state in (\n                trading_enums.EvaluatorStates.LONG.value, trading_enums.EvaluatorStates.VERY_LONG.value\n            ) else trading_enums.TradeOrderSide.SELL\n\n            secondary_quantity = None\n            if user_amount := trading_modes.get_user_selected_order_amount(\n                self.trading_mode, trading_enums.TradeOrderSide.BUY\n            ):\n                initial_entry_price = price if self.trading_mode.use_market_entry_orders else \\\n                    trading_personal_data.decimal_adapt_price(\n                        symbol_market,\n                        price * (\n                            1 - self.trading_mode.entry_limit_orders_price_multiplier\n                            if side is trading_enums.TradeOrderSide.BUY\n                            else 1 + self.trading_mode.entry_limit_orders_price_multiplier\n                        )\n                    )\n                if self.trading_mode.cancel_open_orders_at_each_entry:\n                    post_cancel_dependencies = await self._cancel_existing_orders_if_replaceable(\n                        ctx, symbol, user_amount, price, initial_entry_price, \n                        side, symbol_market, initial_dependencies\n                    )\n\n                quantity = await script_keywords.get_amount_from_input_amount(\n                    context=ctx,\n                    input_amount=user_amount,\n                    side=side.value,\n                    reduce_only=False,\n                    is_stop_order=False,\n                    use_total_holding=False,\n                )\n\n                if self.trading_mode.use_secondary_entry_orders and self.trading_mode.secondary_entry_orders_amount:\n                    # compute secondary orders quantity before locking quantity from initial order\n                    secondary_quantity = await script_keywords.get_amount_from_input_amount(\n                        context=ctx,\n                        input_amount=self.trading_mode.secondary_entry_orders_amount,\n                        side=side.value,\n                        reduce_only=False,\n                        is_stop_order=False,\n                        use_total_holding=False,\n                    )\n            else:\n                self.logger.error(\n                        f\"Missing {side.value} entry order quantity in {self.trading_mode.get_name()} configuration\"\n                        f\", please set the \\\"Amount per buy order\\\" value.\")\n                return []\n\n            # consider holdings only after orders have been cancelled\n            current_symbol_holding, current_market_holding, market_quantity = (\n                trading_personal_data.get_portfolio_amounts(\n                    self.exchange_manager, symbol, price\n                )\n            )\n            if self.exchange_manager.is_future:\n                self.trading_mode.ensure_supported(symbol)\n                # on futures, current_symbol_holding = current_market_holding = market_quantity\n                initial_available_base_funds, _ = trading_personal_data.get_futures_max_order_size(\n                    self.exchange_manager, symbol, side,\n                    price, False, current_symbol_holding, market_quantity\n                )\n                initial_available_quote_funds = initial_available_base_funds * price\n            else:\n                initial_available_quote_funds = current_market_holding \\\n                    if side is trading_enums.TradeOrderSide.BUY else current_symbol_holding\n            if side is trading_enums.TradeOrderSide.BUY:\n                initial_entry_order_type = trading_enums.TraderOrderType.BUY_MARKET \\\n                    if self.trading_mode.use_market_entry_orders else trading_enums.TraderOrderType.BUY_LIMIT\n            else:\n                initial_entry_order_type = trading_enums.TraderOrderType.SELL_MARKET \\\n                    if self.trading_mode.use_market_entry_orders else trading_enums.TraderOrderType.SELL_LIMIT\n            adapted_entry_quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                self.exchange_manager, symbol, initial_entry_order_type, quantity, initial_entry_price, side\n            )\n\n            # initial entry\n            orders_should_have_been_created = await self._create_entry_order(\n                initial_entry_order_type, adapted_entry_quantity, initial_entry_price,\n                symbol_market, symbol, created_orders, price, post_cancel_dependencies\n            )\n            # secondary entries\n            if self.trading_mode.use_secondary_entry_orders and self.trading_mode.secondary_entry_orders_count > 0:\n                secondary_order_type = trading_enums.TraderOrderType.BUY_LIMIT \\\n                    if side is trading_enums.TradeOrderSide.BUY else trading_enums.TraderOrderType.SELL_LIMIT\n                if not secondary_quantity:\n                    if self.trading_mode.secondary_entry_orders_amount:\n                        self.logger.warning(\n                            f\"Impossible to create {side.value} secondary entry order: computed quantity is {secondary_quantity}, \"\n                            f\"configured quantity is: {self.trading_mode.secondary_entry_orders_amount}.\"\n                        )\n                    else:\n                        self.logger.error(\n                            f\"Missing {side.value} secondary entry order quantity in {self.trading_mode.get_name()} \"\n                            f\"configuration, please set the \\\"Secondary entry orders count\\\" value \"\n                            f\"when enabling secondary entry orders.\"\n                        )\n                else:\n                    for i in range(self.trading_mode.secondary_entry_orders_count):\n                        remaining_funds = initial_available_quote_funds - sum(\n                            trading_personal_data.get_locked_funds(order)\n                            for order in created_orders\n                        )\n                        adapted_secondary_quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                            self.exchange_manager, symbol, initial_entry_order_type, secondary_quantity,\n                            initial_entry_price, side\n                        )\n                        skip_other_orders = adapted_secondary_quantity != secondary_quantity\n                        if skip_other_orders or remaining_funds < (\n                            (secondary_quantity * initial_entry_price)\n                            if side is trading_enums.TradeOrderSide.BUY else secondary_quantity\n                        ):\n                            self.logger.debug(\n                                f\"Not enough available funds to create {symbol} {i + 1}/\"\n                                f\"{self.trading_mode.secondary_entry_orders_count} secondary order with quantity of \"\n                                f\"{secondary_quantity} on {self.exchange_manager.exchange_name}\"\n                            )\n                            continue\n                        multiplier = self.trading_mode.entry_limit_orders_price_multiplier + \\\n                            (i + 1) * self.trading_mode.secondary_entry_orders_price_multiplier\n                        secondary_target_price = price * (\n                            (1 - multiplier) if side is trading_enums.TradeOrderSide.BUY else\n                            (1 + multiplier)\n                        )\n                        if not await self._create_entry_order(\n                            secondary_order_type, secondary_quantity, secondary_target_price,\n                            symbol_market, symbol, created_orders, price, post_cancel_dependencies\n                        ):\n                            # stop iterating if an order can't be created\n                            self.logger.info(\n                                f\"Stopping {self.exchange_manager.exchange_name} {symbol} entry orders creation \"\n                                f\"on secondary order {i + 1}/{self.trading_mode.secondary_entry_orders_count}.\"\n                            )\n                            break\n            if created_orders:\n                return created_orders\n            if orders_should_have_been_created:\n                raise trading_errors.OrderCreationError()\n            raise trading_errors.MissingMinimalExchangeTradeVolume()\n\n        except (trading_errors.MissingFunds,\n                trading_errors.MissingMinimalExchangeTradeVolume,\n                trading_errors.OrderCreationError,\n                trading_errors.InvalidCancelPolicyError):\n            raise\n        except Exception as err:\n            self.logger.exception(\n                err, True, f\"Failed to create order : {err}. Order: {current_order if current_order else None}\"\n            )\n            return []\n\n    async def _cancel_existing_orders_if_replaceable(\n        self, ctx, symbol, user_amount, price, initial_entry_price, \n        side, symbol_market, dependencies\n    ) -> typing.Optional[commons_signals.SignalDependencies]:\n        next_step_dependencies = None\n        if to_cancel_orders := [\n            order\n            for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol)\n            if not (order.is_cancelled() or order.is_closed() or order.is_partially_filled()) and side is order.side\n        ]:\n            # Cancel existing DCA orders of the same side from previous iterations\n            # Edge cases about cancelling existing orders when recreating entry orders\n            # 1. max holding ratio is reached, meaning that portfolio + open orders already contain the\n            # max % of asset\n            #   => in this case, we still want to be able to replace open orders of any.\n            #   Need to cancel open orders 1st\n            # 2. value of the portfolio or available holdings dropped to the point that user configured\n            # amount\n            # is now too small to comply with min exchange rules.\n            #   => in this case, orders won't be able to be created.\n            #   Open orders should not be cancelled\n            # Conclusion:\n            #   => Always cancel orders first except when exchange min amount would be reached in new\n            #   buy orders\n            next_step_dependencies = commons_signals.SignalDependencies()\n            can_create_entries = await self._can_create_entry_orders_regarding_min_exchange_order_size(\n                ctx, user_amount, price, initial_entry_price, side, symbol_market, to_cancel_orders\n            )\n            if can_create_entries:\n                for order in to_cancel_orders:\n                    try:\n                        is_cancelled, new_dependencies = await self.trading_mode.cancel_order(\n                            order, dependencies=dependencies\n                        )\n                        if is_cancelled:\n                            next_step_dependencies.extend(new_dependencies)\n                    except trading_errors.UnexpectedExchangeSideOrderStateError as err:\n                        self.logger.warning(f\"Skipped order cancel: {err}, order: {order}\")\n            else:\n                self.logger.info(\n                    f\"Skipping {self.exchange_manager.exchange_name} {symbol} entry order cancel as new \"\n                    f\"entries are likely not complying with exchange minimal order size.\"\n                )\n        return next_step_dependencies or dependencies\n\n    async def _can_create_entry_orders_regarding_min_exchange_order_size(\n        self, ctx, user_amount, price, initial_entry_price, side, symbol_market, to_cancel_orders\n    ):\n        quantity = await script_keywords.get_amount_from_input_amount(\n            context=ctx,\n            input_amount=user_amount,\n            side=side.value,\n            reduce_only=False,\n            is_stop_order=False,\n            use_total_holding=False,\n            orders_to_be_ignored=to_cancel_orders,  # consider existing orders as cancelled\n        )\n        can_create_entries = self._is_above_exchange_min_order_size(quantity, initial_entry_price, symbol_market)\n        if (\n            can_create_entries and\n            self.trading_mode.use_secondary_entry_orders and\n            self.trading_mode.secondary_entry_orders_amount\n        ):\n            # compute secondary orders quantity before locking quantity from initial order\n            if secondary_quantity := await script_keywords.get_amount_from_input_amount(\n                context=ctx,\n                input_amount=self.trading_mode.secondary_entry_orders_amount,\n                side=side.value,\n                reduce_only=False,\n                is_stop_order=False,\n                use_total_holding=False,\n                orders_to_be_ignored=to_cancel_orders,  # consider existing orders as cancelled\n            ):\n                # check that at least the 1st secondary order can be created\n                multiplier = self.trading_mode.entry_limit_orders_price_multiplier + (\n                    1 * self.trading_mode.secondary_entry_orders_price_multiplier\n                )\n                secondary_target_price = price * (\n                    (1 - multiplier) if side is trading_enums.TradeOrderSide.BUY else\n                    (1 + multiplier)\n                )\n                can_create_entries = self._is_above_exchange_min_order_size(\n                    secondary_quantity, secondary_target_price, symbol_market\n                )\n        return can_create_entries\n\n    def _is_above_exchange_min_order_size(self, quantity, price, symbol_market):\n        return bool(\n            trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                quantity,\n                price,\n                symbol_market\n            )\n        )\n\n    async def _create_entry_order(\n        self, order_type, quantity, price, symbol_market, \n        symbol, created_orders, current_price, dependencies\n    ):\n        if self._is_max_asset_ratio_reached(symbol):\n            # do not create entry on symbol when max ratio is reached\n            return False\n        for order_quantity, order_price in \\\n                trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    quantity,\n                    price,\n                    symbol_market\n                ):\n            entry_order = trading_personal_data.create_order_instance(\n                trader=self.exchange_manager.trader,\n                order_type=order_type,\n                symbol=symbol,\n                current_price=current_price,\n                quantity=order_quantity,\n                price=order_price\n            )\n            created_at_least_one_order = False\n            try:\n                if created_order := await self._create_entry_with_chained_exit_orders(\n                    entry_order, price, symbol_market, dependencies\n                ):\n                    created_orders.append(created_order)\n                    created_at_least_one_order = True\n                    return True\n            except trading_errors.MaxOpenOrderReachedForSymbolError as err:\n                self.logger.warning(\n                    f\"Impossible to create {symbol} entry ({entry_order.side.value}) order: \"\n                    f\"creating more orders would exceed {self.exchange_manager.exchange_name}'s limits: {err}\"\n                )\n                return created_at_least_one_order\n        try:\n            buying = order_type in (trading_enums.TraderOrderType.BUY_MARKET, trading_enums.TraderOrderType.BUY_LIMIT)\n            parsed_symbol = symbol_util.parse_symbol(symbol)\n            missing_currency = parsed_symbol.quote if buying else parsed_symbol.base\n            settlement_asset = parsed_symbol.settlement_asset if parsed_symbol.is_future() else parsed_symbol.quote\n            quantity_currency = trading_personal_data.get_order_quantity_currency(self.exchange_manager, symbol)\n            if parsed_symbol.is_spot():\n                cost = quantity * price\n            else:\n                cost = quantity\n            min_cost = trading_personal_data.get_minimal_order_cost(symbol_market, default_price=float(price))\n            min_amount = trading_personal_data.get_minimal_order_amount(symbol_market)\n            self.logger.info(\n                f\"Please get more {missing_currency}: {symbol} {order_type.value} not created on \"\n                f\"{self.exchange_manager.exchange_name}: exchange order requirements are not met. \"\n                f\"Attempted order cost: {cost} {settlement_asset}, quantity: {quantity} {quantity_currency}, \"\n                f\"price: {price}, min cost: {min_cost} {settlement_asset}, min amount: {min_amount} {quantity_currency}\"\n            )\n        except Exception as err:\n            self.logger.exception(err, True, f\"Error when creating error message {err}\")\n        return False\n\n    async def _create_entry_with_chained_exit_orders(\n        self, entry_order, entry_price, symbol_market, dependencies\n    ):\n        params = {}\n        exit_side = (\n            trading_enums.TradeOrderSide.SELL \n            if entry_order.side is trading_enums.TradeOrderSide.BUY\n            else trading_enums.TradeOrderSide.BUY\n        )\n        exit_multiplier_side_flag = 1 if exit_side is trading_enums.TradeOrderSide.SELL else -1\n        total_exists_count = 1 + (\n            self.trading_mode.secondary_exit_orders_count if self.trading_mode.use_secondary_exit_orders else 0\n        )\n        stop_price = entry_price * (\n            trading_constants.ONE - (\n                self.trading_mode.stop_loss_price_multiplier * exit_multiplier_side_flag\n            )\n        )\n        first_sell_price = entry_price * (\n            trading_constants.ONE + (\n                self.trading_mode.exit_limit_orders_price_multiplier * exit_multiplier_side_flag\n            )\n        )\n        last_sell_price = entry_price * (\n            trading_constants.ONE + (\n                self.trading_mode.secondary_exit_orders_price_multiplier *\n                (1 + self.trading_mode.secondary_exit_orders_count) * exit_multiplier_side_flag\n            )\n        )\n        # split entry into multiple exits if necessary (and possible)\n        exit_quantities = self._split_entry_quantity(\n            entry_order.origin_quantity, total_exists_count,\n            min(stop_price, first_sell_price, last_sell_price),\n            max(stop_price, first_sell_price, last_sell_price),\n            symbol_market\n        )\n        can_bundle_exit_orders = len(exit_quantities) == 1\n        reduce_only_chained_orders = self.exchange_manager.is_future\n        exit_orders = []\n        # 1. ensure entry order can be created\n        if entry_order.order_type not in (\n            trading_enums.TraderOrderType.BUY_MARKET, trading_enums.TraderOrderType.SELL_MARKET\n        ):\n            trading_personal_data.ensure_orders_limit(\n                self.exchange_manager, entry_order.symbol, [trading_enums.TraderOrderType.BUY_LIMIT]\n            )\n        for i, exit_quantity in exit_quantities:\n            is_last = i == len(exit_quantities)\n            order_couple = []\n            # stop loss\n            if self.trading_mode.use_stop_loss:\n                # 1. ensure order can be created\n                exit_orders.append(trading_enums.TraderOrderType.STOP_LOSS)\n                trading_personal_data.ensure_orders_limit(self.exchange_manager, entry_order.symbol, exit_orders)\n                # 2. initialize order\n                stop_price = trading_personal_data.decimal_adapt_price(symbol_market, stop_price)\n                param_update, chained_order = await self.register_chained_order(\n                    entry_order, stop_price, trading_enums.TraderOrderType.STOP_LOSS, exit_side,\n                    quantity=exit_quantity, allow_bundling=can_bundle_exit_orders,\n                    reduce_only=reduce_only_chained_orders,\n                    # only the last order is to take trigger fees into account\n                    update_with_triggering_order_fees=is_last and not self.exchange_manager.is_future\n                )\n                params.update(param_update)\n                order_couple.append(chained_order)\n            # take profit\n            if self.trading_mode.use_take_profit_exit_orders:\n                # 1. ensure order can be created\n                take_profit_order_type = self.exchange_manager.trader.get_take_profit_order_type(\n                    entry_order,\n                    trading_enums.TraderOrderType.BUY_LIMIT\n                    if exit_side is trading_enums.TradeOrderSide.BUY else trading_enums.TraderOrderType.SELL_LIMIT\n                )\n                exit_orders.append(take_profit_order_type)\n                trading_personal_data.ensure_orders_limit(self.exchange_manager, entry_order.symbol, exit_orders)\n                # 2. initialize order\n                take_profit_multiplier = self.trading_mode.exit_limit_orders_price_multiplier \\\n                    if i == 1 else (\n                        self.trading_mode.exit_limit_orders_price_multiplier +\n                        self.trading_mode.secondary_exit_orders_price_multiplier * i\n                    )\n                take_profit_price = trading_personal_data.decimal_adapt_price(\n                    symbol_market,\n                    entry_price * (\n                            trading_constants.ONE + (take_profit_multiplier * exit_multiplier_side_flag)\n                    )\n                )\n                param_update, chained_order = await self.register_chained_order(\n                    entry_order, take_profit_price, take_profit_order_type, None,\n                    quantity=exit_quantity, allow_bundling=can_bundle_exit_orders,\n                    reduce_only=reduce_only_chained_orders,\n                    # only the last order is to take trigger fees into account\n                    update_with_triggering_order_fees=is_last and not self.exchange_manager.is_future\n                )\n                params.update(param_update)\n                order_couple.append(chained_order)\n            if len(order_couple) > 1:\n                oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(\n                    trading_personal_data.OneCancelsTheOtherOrderGroup,\n                    active_order_swap_strategy=trading_personal_data.StopFirstActiveOrderSwapStrategy()\n                )\n                for order in order_couple:\n                    order.add_to_order_group(oco_group)\n                # in futures, inactive orders are not necessary\n                if self.exchange_manager.trader.enable_inactive_orders and not self.exchange_manager.is_future:\n                    await oco_group.active_order_swap_strategy.apply_inactive_orders(order_couple)\n        return await self.trading_mode.create_order(\n            entry_order, params=params or None, dependencies=dependencies\n        )\n\n    def _is_max_asset_ratio_reached(self, symbol):\n        if self.exchange_manager.is_future:\n            # not implemented for futures\n            return False\n        asset = symbol_util.parse_symbol(symbol).base\n        ratio = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n            portfolio_value_holder.get_holdings_ratio(asset, include_assets_in_open_orders=True)\n        if ratio >= self.trading_mode.max_asset_holding_ratio:\n            self.logger.info(\n                f\"Max holding ratio reached for {asset}: ratio: {ratio}, max ratio: \"\n                f\"{self.trading_mode.max_asset_holding_ratio}. Skipping {symbol} entry order.\"\n            )\n            return True\n        return False\n\n    @staticmethod\n    def _split_entry_quantity(quantity, target_exits_count, lowest_price, highest_price, symbol_market):\n        if target_exits_count == 1:\n            return [(1, quantity)]\n        adapted_sell_orders_count, increment = trading_personal_data.get_split_orders_count_and_increment(\n            lowest_price, highest_price, quantity, target_exits_count, symbol_market, False\n        )\n        if adapted_sell_orders_count:\n            return [\n                (\n                    i + 1,\n                    trading_personal_data.decimal_adapt_quantity(symbol_market, quantity / adapted_sell_orders_count)\n                )\n                for i in range(adapted_sell_orders_count)\n            ]\n        else:\n            return []\n\n    def skip_portfolio_available_check_before_creating_orders(self) -> bool:\n        \"\"\"\n        When returning true, will skip portfolio available funds check\n        before calling self.create_new_orders().\n        Override if necessary\n        \"\"\"\n        # will cancel open orders: skip available checks\n        return self.trading_mode.cancel_open_orders_at_each_entry\n\n\nclass DCATradingModeProducer(trading_modes.AbstractTradingModeProducer):\n    MINUTES_BEFORE_NEXT_BUY = \"minutes_before_next_buy\"\n    TRIGGER_MODE = \"trigger_mode\"\n    CANCEL_OPEN_ORDERS_AT_EACH_ENTRY = \"cancel_open_orders_at_each_entry\"\n    HEALTH_CHECK_ORPHAN_FUNDS_THRESHOLD = \"health_check_orphan_funds_threshold\"\n    MAX_ASSET_HOLDING_PERCENT = \"max_asset_holding_percent\"\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        self.task = None\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        if self.task is not None:\n            self.task.cancel()\n        await super().stop()\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        evaluations = []\n        # Strategies analysis\n        for evaluated_strategy_node in matrix.get_tentacles_value_nodes(\n                matrix_id,\n                matrix.get_tentacle_nodes(matrix_id,\n                                          exchange_name=self.exchange_name,\n                                          tentacle_type=evaluators_enums.EvaluatorMatrixTypes.STRATEGIES.value),\n                cryptocurrency=cryptocurrency,\n                symbol=symbol):\n\n            if evaluators_util.check_valid_eval_note(evaluators_api.get_value(evaluated_strategy_node),\n                                                     evaluators_api.get_type(evaluated_strategy_node),\n                                                     evaluators_constants.EVALUATOR_EVAL_DEFAULT_TYPE):\n                evaluations.append(evaluators_api.get_value(evaluated_strategy_node))\n\n        is_forced_init_entry = self._should_trigger_init_entry()\n        if evaluations or is_forced_init_entry:\n            state = trading_enums.EvaluatorStates.NEUTRAL\n            if is_forced_init_entry:\n                self.logger.info(\n                    f\"Triggering {self.trading_mode.symbol} init entries [{self.exchange_manager.exchange_name}]\"\n                )\n                state = trading_enums.EvaluatorStates.VERY_LONG\n            elif all(\n                evaluation == -1\n                for evaluation in evaluations\n            ):\n                state = trading_enums.EvaluatorStates.VERY_LONG\n            elif all(\n                evaluation == 1\n                for evaluation in evaluations\n            ):\n                state = trading_enums.EvaluatorStates.VERY_SHORT\n            self.final_eval = evaluations\n            try:\n                await self.trigger_dca(cryptocurrency=cryptocurrency, symbol=symbol, state=state)\n            finally:\n                try:\n                    self.trading_mode.are_initialization_orders_pending = False\n                except AttributeError:\n                    if self.trading_mode is None:\n                        # can very rarely happen on early cancelled backtestings\n                        self.logger.warning(\n                            f\"{self.__class__.__name__} has already been stopped, skipping are_initialization_orders_pending setting\"\n                        )\n                    else:\n                        # unexpected error, raise\n                        raise\n\n    def _should_trigger_init_entry(self):\n        if self.trading_mode.enable_initialization_entry:\n            return self.trading_mode.are_initialization_orders_pending\n        return False\n\n    async def trigger_dca(self, cryptocurrency: str, symbol: str, state: trading_enums.EvaluatorStates):\n        if self.trading_mode.max_asset_holding_ratio < trading_constants.ONE:\n            # if holding ratio should be checked, wait for price init to be able to compute this ratio\n            await self._wait_for_symbol_prices_and_profitability_init(self._get_config_init_timeout())\n        self.state = state\n        self.logger.debug(\n            f\"{symbol} DCA triggered on {self.exchange_manager.exchange_name}, state: {self.state.value}\"\n        )\n        if self.state is trading_enums.EvaluatorStates.NEUTRAL:\n            self.last_activity = trading_modes.TradingModeActivity(trading_enums.TradingModeActivityType.NOTHING_TO_DO)\n        else:\n            self.last_activity = trading_modes.TradingModeActivity(trading_enums.TradingModeActivityType.CREATED_ORDERS)\n            await self._process_entries(cryptocurrency, symbol, state)\n            await self._process_exits(cryptocurrency, symbol, state)\n\n    async def _process_pre_entry_actions(self, symbol: str, side=trading_enums.PositionSide.BOTH):\n        try:\n            # if position is idle, ensure leverage is set according to configuration\n            if (\n                self.exchange_manager.is_future and\n                self.exchange_manager.exchange_personal_data.positions_manager.get_symbol_position(\n                    symbol, side\n                ).is_idle()\n            ):\n                config_leverage = await script_keywords.user_select_leverage(\n                    script_keywords.get_base_context(self.trading_mode, symbol=symbol), def_val=0\n                )\n                if config_leverage:\n                    parsed_leverage = decimal.Decimal(str(config_leverage))\n                    current_leverage = self.exchange_manager.exchange.get_pair_future_contract(symbol).current_leverage\n                    if parsed_leverage != current_leverage:\n                        self.logger.info(f\"Updating leverage of {symbol} from {current_leverage} to {parsed_leverage}\")\n                        await self.trading_mode.set_leverage(symbol, side, parsed_leverage)\n        except Exception as err:\n            self.logger.exception(\n                err, True, f\"Error when processing pre_state_update_actions: {err} ({symbol=} {side=})\"\n            )\n\n    async def _process_entries(self, cryptocurrency: str, symbol: str, state: trading_enums.EvaluatorStates):\n        entry_side = trading_enums.TradeOrderSide.BUY if state in (\n            trading_enums.EvaluatorStates.LONG, trading_enums.EvaluatorStates.VERY_LONG\n        ) else trading_enums.TradeOrderSide.SELL\n        if entry_side is trading_enums.TradeOrderSide.SELL:\n            self.logger.debug(f\"{entry_side.value} entry side not supported for now. Ignored state: {state.value})\")\n            return\n        await self._process_pre_entry_actions(symbol)\n        # call orders creation from consumers\n        await self.submit_trading_evaluation(\n            cryptocurrency=cryptocurrency,\n            symbol=symbol,\n            time_frame=None,\n            final_note=None,\n            state=state\n        )\n        # send_notification\n        await self._send_alert_notification(symbol, state, \"entry\")\n\n    async def _process_exits(self, cryptocurrency: str, symbol: str, state: trading_enums.EvaluatorStates):\n        # todo implement signal based exits\n        pass\n\n    async def dca_task(self):\n        while not self.should_stop:\n            try:\n                for cryptocurrency, pairs in trading_util.get_traded_pairs_by_currency(\n                        self.exchange_manager.config\n                ).items():\n                    if self.trading_mode.symbol in pairs:\n                        await self.trigger_dca(\n                            cryptocurrency=cryptocurrency,\n                            symbol=self.trading_mode.symbol,\n                            state=trading_enums.EvaluatorStates.VERY_LONG\n                        )\n                if self.exchange_manager.is_backtesting:\n                    self.logger.error(\n                        f\"{self.trading_mode.trigger_mode.value} trigger is not supporting backtesting for now. Please \"\n                        f\"configure another trigger mode to use {self.trading_mode.get_name()} in backtesting.\"\n                    )\n                    return\n                await asyncio.sleep(self.trading_mode.minutes_before_next_buy * commons_constants.MINUTE_TO_SECONDS)\n            except Exception as e:\n                self.logger.error(f\"An error happened during DCA task : {e}\")\n\n    async def inner_start(self) -> None:\n        await super().inner_start()\n        if self.trading_mode.trigger_mode is TriggerMode.TIME_BASED:\n            self.task = asyncio.create_task(self.delayed_start())\n\n    def get_channels_registration(self):\n        registration_channels = []\n        if self.trading_mode.trigger_mode is TriggerMode.MAXIMUM_EVALUATORS_SIGNALS_BASED:\n            topic = self.trading_mode.trading_config.get(commons_constants.CONFIG_ACTIVATION_TOPICS.replace(\" \", \"_\"),\n                                                         commons_enums.ActivationTopics.EVALUATION_CYCLE.value)\n            try:\n                registration_channels.append(self.TOPIC_TO_CHANNEL_NAME[topic])\n            except KeyError:\n                self.logger.error(f\"Unknown registration topic: {topic}\")\n        return registration_channels\n\n    def get_extra_init_symbol_topics(self) -> typing.Optional[list]:\n        if self.exchange_manager.is_backtesting:\n            # disabled in backtesting as price might not be initialized at this point\n            return None\n        # required as trigger can happen independently of price events when time based\n        return [commons_enums.InitializationEventExchangeTopics.PRICE.value]\n\n    async def delayed_start(self):\n        await self.dca_task()\n\n    async def _send_alert_notification(self, symbol, state, step):\n        if self.exchange_manager.is_backtesting:\n            return\n        try:\n            import octobot_services.api as services_api\n            import octobot_services.enums as services_enum\n            action = \"unknown\"\n            if state in (trading_enums.EvaluatorStates.LONG, trading_enums.EvaluatorStates.VERY_LONG):\n                action = \"BUYING\"\n            elif state in (trading_enums.EvaluatorStates.SHORT, trading_enums.EvaluatorStates.VERY_SHORT):\n                action = \"SELLING\"\n            title = f\"DCA {step} trigger for : #{symbol}\"\n            alert = f\"{action} on {self.exchange_manager.exchange_name}\"\n            await services_api.send_notification(services_api.create_notification(\n                alert, title=title, markdown_text=alert,\n                category=services_enum.NotificationCategory.PRICE_ALERTS\n            ))\n        except ImportError as e:\n            self.logger.exception(e, True, f\"Impossible to send notification: {e}\")\n\n\nclass DCATradingMode(trading_modes.AbstractTradingMode):\n    MODE_PRODUCER_CLASSES = [DCATradingModeProducer]\n    MODE_CONSUMER_CLASSES = [DCATradingModeConsumer]\n    SUPPORTS_INITIAL_PORTFOLIO_OPTIMIZATION = True\n    SUPPORTS_HEALTH_CHECK = True\n    DEFAULT_HEALTH_CHECK_SELL_ORPHAN_FUNDS_RATIO_THRESHOLD = decimal.Decimal(\"0.1\")  # 10%\n    HEALTH_CHECK_FILL_ORDERS_TIMEOUT = 20\n\n    def __init__(self, config, exchange_manager):\n        super().__init__(config, exchange_manager)\n        self.enable_initialization_entry = False\n        self.use_market_entry_orders = False\n        self.trigger_mode = TriggerMode.TIME_BASED\n        self.minutes_before_next_buy = None\n\n        self.entry_limit_orders_price_multiplier = DCATradingModeConsumer.DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n        self.use_secondary_entry_orders = False\n        self.secondary_entry_orders_count = DCATradingModeConsumer.DEFAULT_SECONDARY_ENTRY_ORDERS_COUNT\n        self.secondary_entry_orders_amount = DCATradingModeConsumer.DEFAULT_SECONDARY_ENTRY_ORDERS_AMOUNT\n        self.secondary_entry_orders_price_multiplier = DCATradingModeConsumer.DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n\n        self.use_take_profit_exit_orders = False\n        self.exit_limit_orders_price_multiplier = DCATradingModeConsumer.DEFAULT_EXIT_LIMIT_PRICE_MULTIPLIER\n        self.use_secondary_exit_orders = False\n        self.secondary_exit_orders_count = DCATradingModeConsumer.DEFAULT_SECONDARY_EXIT_ORDERS_COUNT\n        self.secondary_exit_orders_price_multiplier = DCATradingModeConsumer.DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER\n\n        self.use_stop_loss = False\n        self.stop_loss_price_multiplier = DCATradingModeConsumer.DEFAULT_STOP_LOSS_ORDERS_PRICE_MULTIPLIER\n\n        self.cancel_open_orders_at_each_entry = True\n        self.health_check_orphan_funds_threshold = self.DEFAULT_HEALTH_CHECK_SELL_ORPHAN_FUNDS_RATIO_THRESHOLD\n        self.max_asset_holding_ratio = trading_constants.ONE\n        self.max_asset_holding_ratio = decimal.Decimal(\"0.5\")\n        # self.max_asset_holding_ratio = decimal.Decimal(\"0.66\")\n        # self.max_asset_holding_ratio = decimal.Decimal(\"1\")\n\n        # enable initialization orders\n        self.are_initialization_orders_pending = True\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        default_config = self.get_default_config()\n        self.trigger_mode = TriggerMode(\n            self.UI.user_input(\n                DCATradingModeProducer.TRIGGER_MODE, commons_enums.UserInputTypes.OPTIONS,\n                default_config[DCATradingModeProducer.TRIGGER_MODE],\n                inputs, options=[mode.value for mode in TriggerMode],\n                title=\"Trigger mode: When should DCA entry orders should be triggered.\"\n            )\n        )\n        self.minutes_before_next_buy = int(self.UI.user_input(\n            DCATradingModeProducer.MINUTES_BEFORE_NEXT_BUY, commons_enums.UserInputTypes.INT,\n            default_config[DCATradingModeProducer.MINUTES_BEFORE_NEXT_BUY], inputs,\n            min_val=1,\n            title=\"Trigger period: Minutes to wait between each transaction. Examples: 60 for 1 hour, 1440 for 1 day, \"\n                  \"10080 for 1 week or 43200 for 1 month.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                    DCATradingModeProducer.TRIGGER_MODE: TriggerMode.TIME_BASED.value\n                }\n            }\n        ))\n        self.enable_initialization_entry = self.UI.user_input(\n            DCATradingModeConsumer.USE_INIT_ENTRY_ORDERS, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeConsumer.USE_INIT_ENTRY_ORDERS], inputs,\n            title=\"Enable initialization entry orders: Automatically trigger entry orders \"\n                  \"when starting OctoBot, regardless of initial evaluator values.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                    DCATradingModeProducer.TRIGGER_MODE: TriggerMode.MAXIMUM_EVALUATORS_SIGNALS_BASED.value\n                }\n            }\n        )\n        trading_modes.user_select_order_amount(self, inputs, include_sell=False)\n        self.use_market_entry_orders = self.UI.user_input(\n            DCATradingModeConsumer.USE_MARKET_ENTRY_ORDERS, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeConsumer.USE_MARKET_ENTRY_ORDERS], inputs,\n            title=\"Use market orders instead of limit orders.\"\n        )\n        self.entry_limit_orders_price_multiplier = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeConsumer.ENTRY_LIMIT_ORDERS_PRICE_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeConsumer.ENTRY_LIMIT_ORDERS_PRICE_PERCENT]\n                      * trading_constants.ONE_HUNDRED), inputs,\n                min_val=0,\n                title=\"Limit entry percent difference: Price difference in percent to compute the entry price from \"\n                      \"when using limit orders. \"\n                      \"Example: 10 on a 2000 USDT price would create a buy limit price at 1800 USDT or \"\n                      \"a sell limit price at 2200 USDT.\",\n                editor_options={\n                    commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                        DCATradingModeConsumer.USE_MARKET_ENTRY_ORDERS: False\n                    }\n                }\n            )\n        )) / trading_constants.ONE_HUNDRED\n        self.use_secondary_entry_orders = self.UI.user_input(\n            DCATradingModeConsumer.USE_SECONDARY_ENTRY_ORDERS, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeConsumer.USE_SECONDARY_ENTRY_ORDERS], inputs,\n            title=\"Enable secondary entry orders: Split entry into multiple orders using different prices.\"\n        )\n        self.secondary_entry_orders_count = self.UI.user_input(\n            DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_COUNT, commons_enums.UserInputTypes.INT,\n            default_config[DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_COUNT], inputs,\n            title=\"Secondary entry orders count: Number of secondary limit orders to create alongside the initial \"\n                  \"entry order.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                    DCATradingModeConsumer.USE_SECONDARY_ENTRY_ORDERS: True\n                }\n            }\n        )\n        self.secondary_entry_orders_price_multiplier = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_PRICE_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_PRICE_PERCENT]\n                      * trading_constants.ONE_HUNDRED), inputs,\n                title=\"Secondary entry orders price interval percent: Price difference in percent to compute the \"\n                      \"price of secondary entry orders compared to the price of the initial entry order. \"\n                      \"Example: 10 on a 1800 USDT entry buy (with an asset price of 2000) would \"\n                      \"create secondary entry buy orders at 1600 USDT, 1400 USDT and so on.\",\n                editor_options={\n                    commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                        DCATradingModeConsumer.USE_SECONDARY_ENTRY_ORDERS: True\n                    }\n                }\n            )\n        )) / trading_constants.ONE_HUNDRED\n        self.secondary_entry_orders_amount = self.UI.user_input(\n            DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_AMOUNT, commons_enums.UserInputTypes.TEXT,\n            default_config[DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_AMOUNT], inputs,\n            title=f\"Secondary entry orders amount: {trading_modes.get_order_amount_value_desc()}\",\n            other_schema_values={\"minLength\": 0},\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                    DCATradingModeConsumer.USE_SECONDARY_ENTRY_ORDERS: True\n                }\n            }\n        )\n        self.use_take_profit_exit_orders = self.UI.user_input(\n            DCATradingModeConsumer.USE_TAKE_PROFIT_EXIT_ORDERS, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeConsumer.USE_TAKE_PROFIT_EXIT_ORDERS], inputs,\n            title=\"Enable take profit exit orders: Automatically create take profit exit orders \"\n                  \"when entries are filled.\"\n        )\n        self.exit_limit_orders_price_multiplier = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeConsumer.EXIT_LIMIT_ORDERS_PRICE_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeConsumer.EXIT_LIMIT_ORDERS_PRICE_PERCENT]\n                      * trading_constants.ONE_HUNDRED), inputs,\n                min_val=0,\n                title=\"Limit exit percent difference: Price difference in percent to compute the exit price from \"\n                      \"after an entry is filled. \"\n                      \"Example: 10 on a 2000 USDT filled price buy would create a sell limit price at 2200 USDT.\",\n                editor_options={\n                    commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                        DCATradingModeConsumer.USE_TAKE_PROFIT_EXIT_ORDERS: True\n                    }\n                }\n            )\n        )) / trading_constants.ONE_HUNDRED\n        self.use_secondary_exit_orders = self.UI.user_input(\n            DCATradingModeConsumer.USE_SECONDARY_EXIT_ORDERS, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeConsumer.USE_SECONDARY_EXIT_ORDERS], inputs,\n            title=\"Enable secondary exit orders: Split each filled entry order into into multiple exit orders using \"\n                  \"different prices.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                    DCATradingModeConsumer.USE_TAKE_PROFIT_EXIT_ORDERS: True\n                }\n            }\n        )\n        self.secondary_exit_orders_count = self.UI.user_input(\n            DCATradingModeConsumer.SECONDARY_EXIT_ORDERS_COUNT, commons_enums.UserInputTypes.INT,\n            default_config[DCATradingModeConsumer.SECONDARY_EXIT_ORDERS_COUNT], inputs,\n            title=\"Secondary exit orders count: Number of secondary limit orders to create additionally to \"\n                  \"the initial exit order. When enabled, the entry filled amount is split into each exit orders.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                    DCATradingModeConsumer.USE_SECONDARY_EXIT_ORDERS: True\n                }\n            }\n        )\n        self.secondary_exit_orders_price_multiplier = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeConsumer.SECONDARY_EXIT_ORDERS_PRICE_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeConsumer.SECONDARY_EXIT_ORDERS_PRICE_PERCENT]\n                      * trading_constants.ONE_HUNDRED), inputs,\n                title=\"Secondary exit orders price interval percent: Price difference in percent to compute the \"\n                      \"price of secondary exit orders compared to the price of the associated entry order. \"\n                      \"Example: 10 on a 2000 USDT exit sell price would create secondary exit sell orders \"\n                      \"at 2200 USDT, 2400 USDT and so on.\",\n                editor_options={\n                    commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                        DCATradingModeConsumer.USE_SECONDARY_EXIT_ORDERS: True\n                    }\n                }\n            )\n        )) / trading_constants.ONE_HUNDRED\n        self.use_stop_loss = self.UI.user_input(\n            DCATradingModeConsumer.USE_STOP_LOSSES, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeConsumer.USE_STOP_LOSSES], inputs,\n            title=\"Enable stop losses: Create stop losses when entries are filled.\",\n        )\n        self.stop_loss_price_multiplier = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeConsumer.STOP_LOSS_PRICE_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeConsumer.STOP_LOSS_PRICE_PERCENT] * trading_constants.ONE_HUNDRED),\n                inputs, min_val=0, max_val=100,\n                title=\"Stop loss price percent: maximum percent losses to compute the stop loss price from. \"\n                      \"Example: a buy entry filled at 2000 with a Stop loss percent at\"\n                      \" 15 will create a stop order at 1700.\",\n                editor_options={\n                    commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                        DCATradingModeConsumer.USE_STOP_LOSSES: True\n                    }\n                }\n            )\n        )) / trading_constants.ONE_HUNDRED\n\n        self.cancel_open_orders_at_each_entry = self.UI.user_input(\n            DCATradingModeProducer.CANCEL_OPEN_ORDERS_AT_EACH_ENTRY, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[DCATradingModeProducer.CANCEL_OPEN_ORDERS_AT_EACH_ENTRY], inputs,\n            title=\"Cancel open orders on each entry: Cancel existing orders from previous iteration on each entry.\",\n        )\n\n        self.is_health_check_enabled = self.UI.user_input(\n            self.ENABLE_HEALTH_CHECK, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.ENABLE_HEALTH_CHECK], inputs,\n            title=\"Health check: when enabled, OctoBot will automatically sell traded assets that are not associated \"\n                  \"to a sell order and that represent at least the 'Health check threshold' part of the \"\n                  \"portfolio. Health check can be useful to avoid inactive funds, for example if a buy order got \"\n                  \"filled but no sell order was created. Requires a common quote market for each traded pair. \"\n                  \"Warning: will sell any asset associated to a trading pair that is not covered by a sell order, \"\n                  \"even if not bought by OctoBot or this trading mode.\",\n        )\n        self.health_check_orphan_funds_threshold = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeProducer.HEALTH_CHECK_ORPHAN_FUNDS_THRESHOLD, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeProducer.HEALTH_CHECK_ORPHAN_FUNDS_THRESHOLD]\n                      * trading_constants.ONE_HUNDRED), inputs,\n                title=\"Health check threshold: Minimum % of the portfolio taken by a traded asset that is not in \"\n                      \"sell orders. Assets above this threshold will be sold for the common quote market during \"\n                      \"Health check.\",\n                editor_options={\n                    commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                        self.ENABLE_HEALTH_CHECK: True\n                    }\n                }\n            )\n        )) / trading_constants.ONE_HUNDRED\n        self.max_asset_holding_ratio = decimal.Decimal(str(\n            self.UI.user_input(\n                DCATradingModeProducer.MAX_ASSET_HOLDING_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                float(default_config[DCATradingModeProducer.MAX_ASSET_HOLDING_PERCENT]\n                      * trading_constants.ONE_HUNDRED), inputs,\n                title=\"Max asset holding: Maximum % of the portfolio to allocate to an asset. \"\n                      \"Buy orders to buy this asset won't be created if this ratio is reached. \"\n                      \"Only applied when trading on spot.\",\n                min_val=0, max_val=100\n            )\n        )) / trading_constants.ONE_HUNDRED\n\n    @classmethod\n    def get_default_config(\n        cls,\n        buy_amount: typing.Optional[str] = None,\n        sell_amount: typing.Optional[str] = None,\n        use_secondary_entry_orders: typing.Optional[bool] = None,\n        secondary_entry_orders_count: typing.Optional[int] = None,\n        exit_limit_orders_price_percent: typing.Optional[float] = None,\n        entry_limit_orders_price_percent: typing.Optional[float] = None,\n        secondary_entry_orders_price_percent: typing.Optional[float] = None,\n        secondary_entry_orders_amount: typing.Optional[str] = None,\n        enable_stop_loss: typing.Optional[bool] = None,\n        stop_loss_price: typing.Optional[float] = None,\n        use_init_entry_orders: typing.Optional[bool] = None,\n        use_take_profit_exit_orders: typing.Optional[bool] = None,\n        trigger_mode:typing. Optional[TriggerMode] = None,\n        secondary_exit_orders_price_percent: typing.Optional[float] = None,\n        health_check_orphan_funds_threshold: typing.Optional[float] = None,\n        max_asset_holding_percent : typing.Optional[float] = None,\n    ) -> dict:\n        return {\n            trading_constants.CONFIG_BUY_ORDER_AMOUNT: buy_amount,\n            trading_constants.CONFIG_SELL_ORDER_AMOUNT: sell_amount,\n            DCATradingModeProducer.TRIGGER_MODE: trigger_mode.value if trigger_mode else TriggerMode.TIME_BASED.value,\n            DCATradingModeProducer.MINUTES_BEFORE_NEXT_BUY: 10080,\n            DCATradingModeConsumer.USE_INIT_ENTRY_ORDERS: use_init_entry_orders or False,\n            DCATradingModeConsumer.USE_MARKET_ENTRY_ORDERS: False,\n            DCATradingModeConsumer.ENTRY_LIMIT_ORDERS_PRICE_PERCENT:\n                entry_limit_orders_price_percent or DCATradingModeConsumer.DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER,\n            DCATradingModeConsumer.USE_SECONDARY_ENTRY_ORDERS: use_secondary_entry_orders or False,\n            DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_COUNT:\n                secondary_entry_orders_count or DCATradingModeConsumer.DEFAULT_SECONDARY_ENTRY_ORDERS_COUNT,\n            DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_PRICE_PERCENT:\n                secondary_entry_orders_price_percent or DCATradingModeConsumer.DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER,\n            DCATradingModeConsumer.SECONDARY_ENTRY_ORDERS_AMOUNT: secondary_entry_orders_amount or \"\",\n            DCATradingModeConsumer.USE_TAKE_PROFIT_EXIT_ORDERS: use_take_profit_exit_orders or False,\n            DCATradingModeConsumer.EXIT_LIMIT_ORDERS_PRICE_PERCENT:\n                exit_limit_orders_price_percent or DCATradingModeConsumer.DEFAULT_EXIT_LIMIT_PRICE_MULTIPLIER,\n            DCATradingModeConsumer.USE_SECONDARY_EXIT_ORDERS: False,\n            DCATradingModeConsumer.SECONDARY_EXIT_ORDERS_COUNT:\n                DCATradingModeConsumer.DEFAULT_SECONDARY_EXIT_ORDERS_COUNT,\n            DCATradingModeConsumer.SECONDARY_EXIT_ORDERS_PRICE_PERCENT:\n                secondary_exit_orders_price_percent or DCATradingModeConsumer.DEFAULT_ENTRY_LIMIT_PRICE_MULTIPLIER,\n            DCATradingModeConsumer.USE_STOP_LOSSES: enable_stop_loss or False,\n            DCATradingModeConsumer.STOP_LOSS_PRICE_PERCENT:\n                stop_loss_price or DCATradingModeConsumer.DEFAULT_STOP_LOSS_ORDERS_PRICE_MULTIPLIER,\n            DCATradingModeProducer.CANCEL_OPEN_ORDERS_AT_EACH_ENTRY: True,\n            cls.ENABLE_HEALTH_CHECK: False,\n            DCATradingModeProducer.HEALTH_CHECK_ORPHAN_FUNDS_THRESHOLD:\n                health_check_orphan_funds_threshold or cls.DEFAULT_HEALTH_CHECK_SELL_ORPHAN_FUNDS_RATIO_THRESHOLD,\n            DCATradingModeProducer.MAX_ASSET_HOLDING_PERCENT: max_asset_holding_percent or decimal.Decimal(1),\n        }\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_current_state(self) -> (str, float):\n        return (\n            super().get_current_state()[0] if self.producers[0].state is None else self.producers[0].state.name,\n            \",\".join([str(e) for e in self.producers[0].final_eval]) if self.producers[0].final_eval\n            else self.producers[0].final_eval\n        )\n\n    async def single_exchange_process_optimize_initial_portfolio(\n        self, sellable_assets, target_asset: str, tickers: dict\n    ) -> list:\n        traded_coins = [\n            symbol.base\n            for symbol in self.exchange_manager.exchange_config.traded_symbols\n        ]\n        sellable_assets = sorted(list(set(sellable_assets + traded_coins)))\n        self.logger.info(f\"Optimizing portfolio: selling {sellable_assets} to buy {target_asset}\")\n        return await trading_modes.convert_assets_to_target_asset(\n            self, sellable_assets, target_asset, tickers\n        )\n\n    async def single_exchange_process_health_check(self, chained_orders: list, tickers: dict) -> list:\n        common_quote = trading_exchanges.get_common_traded_quote(self.exchange_manager)\n        if (\n            common_quote is None\n            or not (self.use_take_profit_exit_orders or self.use_stop_loss)\n        ):\n            # skipped when:\n            # - common_quote is unset\n            # - not using take profit or stop losses, health check should not be used\n            return []\n        created_orders = []\n        for asset, amount in self._get_lost_funds_to_sell(common_quote, chained_orders):\n            # sell lost funds\n            self.logger.info(\n                f\"Health check: selling {amount} {asset} into {common_quote} on {self.exchange_manager.exchange_name}\"\n            )\n            try:\n                asset_orders = await trading_modes.convert_asset_to_target_asset(\n                    self, asset, common_quote, tickers, asset_amount=amount\n                )\n                if not asset_orders:\n                    self.logger.info(\n                        f\"Health check: Not enough funds to create an order according to exchanges rules using \"\n                        f\"{amount} {asset} into {common_quote} on {self.exchange_manager.exchange_name}\"\n                    )\n                else:\n                    created_orders.extend(asset_orders)\n            except Exception as err:\n                self.logger.exception(\n                    err, True, f\"Error when creating order to sell {asset} into {common_quote}: {err}\"\n                )\n        if created_orders:\n            await asyncio.gather(\n                *[\n                    trading_personal_data.wait_for_order_fill(\n                        order, self.HEALTH_CHECK_FILL_ORDERS_TIMEOUT, True\n                    ) for order in created_orders\n                ]\n            )\n            for producer in self.producers:\n                producer.last_activity = trading_modes.TradingModeActivity(\n                    trading_enums.TradingModeActivityType.CREATED_ORDERS\n                )\n        return created_orders\n\n    def _get_lost_funds_to_sell(self, common_quote: str, chained_orders: list) -> list[(str, decimal.Decimal)]:\n        asset_and_amount = []\n        value_holder = self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder\n        traded_base_assets = set(\n            symbol.base\n            for symbol in self.exchange_manager.exchange_config.traded_symbols\n        )\n        sell_orders = [\n            order\n            for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + chained_orders\n            if order.side is trading_enums.TradeOrderSide.SELL\n        ]\n        partially_filled_buy_orders = [\n            order\n            for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n            if order.side is trading_enums.TradeOrderSide.BUY and order.is_partially_filled()\n        ]\n        orphan_asset_values_by_asset = {}\n        total_traded_assets_value = value_holder.value_converter.evaluate_value(\n            common_quote,\n            self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n                common_quote\n            ).total,\n            target_currency=common_quote,\n            init_price_fetchers=False\n        )\n        for asset in traded_base_assets:\n            asset_holding = \\\n                self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n                    asset\n                )\n            holdings_value = value_holder.value_converter.evaluate_value(\n                asset, asset_holding.total, target_currency=common_quote, init_price_fetchers=False\n            )\n            total_traded_assets_value += holdings_value\n            holdings_in_sell_orders = sum(\n                order.origin_quantity\n                for order in sell_orders\n                if symbol_util.parse_symbol(order.symbol).base == asset\n            )\n            holdings_from_partially_filled_buy_orders = sum(\n                order.filled_quantity\n                for order in partially_filled_buy_orders\n                if symbol_util.parse_symbol(order.symbol).base == asset\n            )\n            # do not consider more than the available amounts\n            orphan_amount = min(\n                asset_holding.total - holdings_in_sell_orders - holdings_from_partially_filled_buy_orders, \n                asset_holding.available\n            )\n            if orphan_amount and orphan_amount > 0:\n                orphan_asset_values_by_asset[asset] = (\n                    holdings_value * orphan_amount / asset_holding.total, orphan_amount\n                )\n\n        for asset, value_and_orphan_amount in orphan_asset_values_by_asset.items():\n            value, orphan_amount = value_and_orphan_amount\n            ratio = value / total_traded_assets_value\n            if ratio > self.health_check_orphan_funds_threshold:\n                asset_and_amount.append((asset, orphan_amount))\n        return asset_and_amount\n"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"DCATradingMode\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/resources/DCATradingMode.md",
    "content": "Dollar cost averaging (DCA) is a trading mode that can help you lower the amount you pay for investments and \nminimize risk. Instead of purchasing investments at a single price point, with dollar cost averaging you buy \nin smaller amounts at regular intervals.\n\n<div class=\"text-center\">\n    <div>\n    <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/519pwSV1uwE?si=MT9e1Gqp9WWw45Z\" \n    title=\"Build your own Smart DCA strategy\" frameborder=\"0\" allow=\"accelerometer; autoplay; \n    clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n    </div>\n</div>\n\nOctoBot's DCA is more than just a simple regular DCA technique, it allows you to accurately automate your \nentries and exit conditions in a simple, yet very powerful way.\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/dca-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=DCATradingModeDocs\">\nfull DCA trading mode guide</a>.\n\n### In a nutshell\n- Entries can be triggered either:\n    - On a pure time base, regardless of price.\n    - Upon enabled evaluators maximum signals (only 1 or -1 evaluations). In this case, the latest evaluation will \n        prevail when using limit entry orders: previous evaluations open orders will be cancelled.\n- Entries can be market or limit orders.\n- Once an entry is filled, you can choose to exit/sell the assets yourself (manually) or automatically \ncreate a take profit at your price target. \n- You can enable stop losses protect your holdings once an entry is filled.\n- It is also possible to split entries and exits into multiple orders at regular price intervals to profit even more \nfrom the dollar cost averaging effect.\n\nOver the long term, dollar cost averaging can help lower your investment costs and boost your returns by optimizing \nentry and exit prices according to your goals.\n\n_Note: When using default configuration, DCA Trading mode will buy 50$ (or unit of the quote currency: USDT for BTC/USDT) \neach week._\n\n\n_This trading mode supports PNL history when take profit exit orders are enabled._\n"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/dca_trading_mode/tests/test_dca_trading_mode.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport pytest_asyncio\nimport os.path\nimport mock\nimport decimal\nimport asyncio\n\nimport async_channel.util as channel_util\n\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.enums as commons_enum\nimport octobot_commons.tests.test_config as test_config\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as commons_symbols\n\nimport octobot_backtesting.api as backtesting_api\n\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.exchange_data as trading_exchange_data\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.modes\nimport octobot_trading.errors\nimport octobot_trading.signals as trading_signals\n\nimport tentacles.Evaluator.TA as TA\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Trading.Mode as Mode\n\nimport tests.test_utils.memory_check_util as memory_check_util\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\n\nimport tentacles.Trading.Mode.dca_trading_mode.dca_trading as dca_trading\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def tools():\n    trader = None\n    try:\n        tentacles_manager_api.reload_tentacle_info()\n        mode, trader = await _get_tools()\n        yield mode, trader\n    finally:\n        if trader:\n            await _stop(trader.exchange_manager)\n\n\n@pytest_asyncio.fixture\nasync def futures_tools():\n    trader = None\n    try:\n        tentacles_manager_api.reload_tentacle_info()\n        mode, trader = await _get_futures_tools()\n        yield mode, trader\n    finally:\n        if trader:\n            await _stop(trader.exchange_manager)\n\n\nasync def test_run_independent_backtestings_with_memory_check():\n    \"\"\"\n    Should always be called first here to avoid other tests' related memory check issues\n    \"\"\"\n    tentacles_setup_config = tentacles_manager_api.create_tentacles_setup_config_with_tentacles(\n        Mode.DCATradingMode,\n        Strategies.SimpleStrategyEvaluator,\n        TA.RSIMomentumEvaluator,\n        TA.EMAMomentumEvaluator\n    )\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_TIME_FRAME] = [commons_enum.TimeFrames.FOUR_HOURS]\n\n    _CONFIG = {\n        Mode.DCATradingMode.get_name(): {\n            \"buy_order_amount\": \"50q\",\n            \"default_config\": [\n                \"SimpleStrategyEvaluator\"\n            ],\n            \"entry_limit_orders_price_percent\": 1,\n            \"exit_limit_orders_price_percent\": 5,\n            \"minutes_before_next_buy\": 10080,\n            \"required_strategies\": [\n                \"SimpleStrategyEvaluator\",\n                \"TechnicalAnalysisStrategyEvaluator\"\n            ],\n            \"secondary_entry_orders_amount\": \"12%\",\n            \"secondary_entry_orders_count\": 0,\n            \"secondary_entry_orders_price_percent\": 5,\n            \"secondary_exit_orders_count\": 2,\n            \"secondary_exit_orders_price_percent\": 5,\n            \"stop_loss_price_percent\": 10,\n            \"trigger_mode\": \"Maximum evaluators signals based\",\n            \"use_market_entry_orders\": False,\n            \"use_secondary_entry_orders\": True,\n            \"use_secondary_exit_orders\": True,\n            \"use_stop_losses\": True,\n            \"use_take_profit_exit_orders\": True\n        },\n        Strategies.SimpleStrategyEvaluator.get_name(): {\n            \"background_social_evaluators\": [\n                \"RedditForumEvaluator\"\n            ],\n            \"default_config\": [\n                \"DoubleMovingAverageTrendEvaluator\",\n                \"RSIMomentumEvaluator\"\n            ],\n            \"re_evaluate_TA_when_social_or_realtime_notification\": True,\n            \"required_candles_count\": 1000,\n            \"required_evaluators\": [\n                \"*\"\n            ],\n            \"required_time_frames\": [\n                \"1h\"\n            ],\n            \"social_evaluators_notification_timeout\": 3600\n        },\n        TA.RSIMomentumEvaluator.get_name(): {\n            \"long_threshold\": 30,\n            \"period_length\": 14,\n            \"short_threshold\": 70,\n            \"trend_change_identifier\": False\n        },\n        TA.EMAMomentumEvaluator.get_name(): {\n            \"period_length\": 14,\n            \"price_threshold_percent\": 2\n        },\n    }\n\n    def config_proxy(tentacles_setup_config, klass):\n        try:\n            return _CONFIG[klass if isinstance(klass, str) else klass.get_name()]\n        except KeyError:\n            return {}\n\n    with tentacles_manager_api.local_tentacle_config_proxy(config_proxy):\n        await memory_check_util.run_independent_backtestings_with_memory_check(config, tentacles_setup_config)\n\n\ndef _get_config(tools, update):\n    mode, trader = tools\n    config = tentacles_manager_api.get_tentacle_config(trader.exchange_manager.tentacles_setup_config, mode.__class__)\n    return {**config, **update}\n\n\nasync def test_init_default_values(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    assert mode.use_market_entry_orders is True\n    assert mode.trigger_mode is dca_trading.TriggerMode.TIME_BASED\n    assert mode.minutes_before_next_buy == 10080\n\n    assert mode.entry_limit_orders_price_multiplier == decimal.Decimal(\"0.05\")\n    assert mode.use_secondary_entry_orders is False\n    assert mode.secondary_entry_orders_count == 0\n    assert mode.secondary_entry_orders_amount == \"\"\n    assert mode.secondary_entry_orders_price_multiplier == decimal.Decimal(\"0.05\")\n\n    assert mode.use_take_profit_exit_orders is False\n    assert mode.exit_limit_orders_price_multiplier == decimal.Decimal(\"0.05\")\n    assert mode.use_secondary_exit_orders is False\n    assert mode.secondary_exit_orders_count == 0\n    assert mode.secondary_exit_orders_price_multiplier == decimal.Decimal(\"0.05\")\n\n    assert mode.use_stop_loss is False\n    assert mode.stop_loss_price_multiplier == decimal.Decimal(\"0.1\")\n\n\nasync def test_init_config_values(tools):\n    update = {\n        \"buy_order_amount\": \"50q\",\n        \"entry_limit_orders_price_percent\": 3,\n        \"exit_limit_orders_price_percent\": 1,\n        \"minutes_before_next_buy\": 333,\n        \"secondary_entry_orders_amount\": \"12%\",\n        \"secondary_entry_orders_count\": 0,\n        \"secondary_entry_orders_price_percent\": 5,\n        \"secondary_exit_orders_count\": 333,\n        \"secondary_exit_orders_price_percent\": 2,\n        \"stop_loss_price_percent\": 10,\n        \"trigger_mode\": \"Maximum evaluators signals based\",\n        \"use_market_entry_orders\": False,\n        \"use_secondary_entry_orders\": True,\n        \"use_secondary_exit_orders\": True,\n        \"use_stop_losses\": True,\n        \"use_take_profit_exit_orders\": True\n    }\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    assert mode.use_market_entry_orders is False\n    assert mode.trigger_mode is dca_trading.TriggerMode.MAXIMUM_EVALUATORS_SIGNALS_BASED\n    assert mode.minutes_before_next_buy == 333\n\n    assert mode.entry_limit_orders_price_multiplier == decimal.Decimal(\"0.03\")\n    assert mode.use_secondary_entry_orders is True\n    assert mode.secondary_entry_orders_count == 0\n    assert mode.secondary_entry_orders_amount == \"12%\"\n    assert mode.secondary_entry_orders_price_multiplier == decimal.Decimal(\"0.05\")\n\n    assert mode.use_take_profit_exit_orders is True\n    assert mode.exit_limit_orders_price_multiplier == decimal.Decimal(\"0.01\")\n    assert mode.use_secondary_exit_orders is True\n    assert mode.secondary_exit_orders_count == 333\n    assert mode.secondary_exit_orders_price_multiplier == decimal.Decimal(\"0.02\")\n\n    assert mode.use_stop_loss is True\n    assert mode.stop_loss_price_multiplier == decimal.Decimal(\"0.1\")\n\n\nasync def test_inner_start(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    with mock.patch.object(producer, \"dca_task\", mock.AsyncMock()) as dca_task_mock, \\\n            mock.patch.object(producer, \"get_channels_registration\", mock.Mock(return_value=[])):\n        # evaluator based\n        mode.trigger_mode = dca_trading.TriggerMode.MAXIMUM_EVALUATORS_SIGNALS_BASED\n        await producer.inner_start()\n        for _ in range(10):\n            await asyncio_tools.wait_asyncio_next_cycle()\n        dca_task_mock.assert_not_called()\n\n        # time based\n        mode.trigger_mode = dca_trading.TriggerMode.TIME_BASED\n        await producer.inner_start()\n        for _ in range(10):\n            await asyncio_tools.wait_asyncio_next_cycle()\n        dca_task_mock.assert_called_once()\n\n\nasync def test_dca_task(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    calls = []\n    try:\n        def _on_trigger(**kwargs):\n            if len(calls):\n                # now stop\n                producer.should_stop = True\n            calls.append(kwargs)\n\n        producer.exchange_manager.is_backtesting = True\n        with mock.patch.object(asyncio, \"sleep\", mock.AsyncMock()) as sleep_mock:\n            # backtesting: trigger only once\n            with mock.patch.object(producer, \"trigger_dca\",\n                                   mock.AsyncMock(side_effect=_on_trigger)) as trigger_dca_mock:\n                await producer.dca_task()\n                assert trigger_dca_mock.call_count == 1\n                assert trigger_dca_mock.mock_calls[0].kwargs == {\n                    \"cryptocurrency\": \"Bitcoin\",\n                    \"symbol\": \"BTC/USDT\",\n                    \"state\": trading_enums.EvaluatorStates.VERY_LONG\n                }\n                sleep_mock.assert_not_called()\n\n            calls.clear()\n            # live: loop trigger\n            producer.exchange_manager.is_backtesting = False\n            with mock.patch.object(producer, \"trigger_dca\",\n                                   mock.AsyncMock(side_effect=_on_trigger)) as trigger_dca_mock:\n                await producer.dca_task()\n                assert trigger_dca_mock.call_count == 2\n                assert trigger_dca_mock.mock_calls[0].kwargs == {\n                    \"cryptocurrency\": \"Bitcoin\",\n                    \"symbol\": \"BTC/USDT\",\n                    \"state\": trading_enums.EvaluatorStates.VERY_LONG\n                }\n                assert sleep_mock.call_count == 2\n                assert sleep_mock.mock_calls[0].args == (10080 * commons_constants.MINUTE_TO_SECONDS,)\n                assert sleep_mock.mock_calls[1].args == (10080 * commons_constants.MINUTE_TO_SECONDS,)\n    finally:\n        producer.exchange_manager.is_backtesting = True\n\n\nasync def test_trigger_dca(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(producer, \"_process_entries\", mock.AsyncMock()) as _process_entries_mock, \\\n            mock.patch.object(producer, \"_process_exits\", mock.AsyncMock()) as _process_exits_mock:\n        producer.last_activity = None\n        await producer.trigger_dca(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.NEUTRAL)\n        assert producer.state is trading_enums.EvaluatorStates.NEUTRAL\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.NOTHING_TO_DO\n        )\n        # neutral is not triggering anything\n        _process_entries_mock.assert_not_called()\n        _process_exits_mock.assert_not_called()\n        producer.last_activity = None\n\n        await producer.trigger_dca(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.LONG)\n        assert producer.state is trading_enums.EvaluatorStates.LONG\n        _process_entries_mock.assert_called_once_with(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.LONG)\n        _process_exits_mock.assert_called_once_with(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.LONG)\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.CREATED_ORDERS\n        )\n        _process_entries_mock.reset_mock()\n        _process_exits_mock.reset_mock()\n        producer.last_activity = None\n\n        await producer.trigger_dca(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.VERY_SHORT)\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.CREATED_ORDERS\n        )\n        assert producer.state is trading_enums.EvaluatorStates.VERY_SHORT\n        _process_entries_mock.assert_called_once_with(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.VERY_SHORT)\n        _process_exits_mock.assert_called_once_with(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.VERY_SHORT)\n\n\nasync def test_process_entries(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(producer, \"submit_trading_evaluation\", mock.AsyncMock()) as submit_trading_evaluation_mock, \\\n            mock.patch.object(producer, \"cancel_symbol_open_orders\",\n                              mock.AsyncMock()) as cancel_symbol_open_orders_mock, \\\n            mock.patch.object(producer, \"_send_alert_notification\", mock.AsyncMock()) as _send_alert_notification_mock, \\\n            mock.patch.object(trader.exchange_manager.exchange_personal_data.positions_manager, \"get_symbol_position\",\n                              mock.AsyncMock()) as get_symbol_position_mock:\n        await producer._process_entries(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.NEUTRAL)\n        # neutral state: does not create orders\n        submit_trading_evaluation_mock.assert_not_called()\n        cancel_symbol_open_orders_mock.assert_not_called()\n        _send_alert_notification_mock.assert_not_called()\n        # spot trading: get_symbol_position is not called by _process_pre_entry_actions\n        get_symbol_position_mock.assert_not_called()\n\n        await producer._process_entries(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.SHORT)\n        await producer._process_entries(\"crypto\", \"symbol\", trading_enums.EvaluatorStates.VERY_SHORT)\n        # short state: not yet supported\n        submit_trading_evaluation_mock.assert_not_called()\n        _send_alert_notification_mock.assert_not_called()\n        get_symbol_position_mock.assert_not_called()\n\n        for state in (trading_enums.EvaluatorStates.LONG, trading_enums.EvaluatorStates.VERY_LONG):\n            await producer._process_entries(\"crypto\", \"symbol\", state)\n            # short state: not yet supported\n            submit_trading_evaluation_mock.assert_called_once_with(\n                cryptocurrency=\"crypto\",\n                symbol=\"symbol\",\n                time_frame=None,\n                final_note=None,\n                state=state\n            )\n            get_symbol_position_mock.assert_not_called()\n            _send_alert_notification_mock.assert_called_once_with(\"symbol\", state, \"entry\")\n            _send_alert_notification_mock.reset_mock()\n            submit_trading_evaluation_mock.reset_mock()\n\n\nasync def test_get_channels_registration(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.trigger_mode = dca_trading.TriggerMode.TIME_BASED\n    assert producer.get_channels_registration() == []\n    mode.trigger_mode = dca_trading.TriggerMode.MAXIMUM_EVALUATORS_SIGNALS_BASED\n    assert producer.get_channels_registration() == [\n        producer.TOPIC_TO_CHANNEL_NAME[commons_enum.ActivationTopics.EVALUATION_CYCLE.value]\n    ]\n\n\nasync def _process_exits(tools):\n    # not implemented\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(producer, \"submit_trading_evaluation\", mock.AsyncMock()) as submit_trading_evaluation_mock:\n        for state in trading_enums.EvaluatorStates:\n            await producer._process_exits(\"crypto\", \"symbol\", state)\n        submit_trading_evaluation_mock.assert_not_called()\n\n\nasync def test_split_entry_quantity(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    symbol = mode.symbol\n    symbol_market = trader.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    assert consumer._split_entry_quantity(\n        decimal.Decimal(\"123\"), 1, decimal.Decimal(\"12\"), decimal.Decimal(\"15\"), symbol_market\n    ) == [(1, decimal.Decimal(\"123\"))]\n    assert consumer._split_entry_quantity(\n        decimal.Decimal(\"123\"), 2, decimal.Decimal(\"12\"), decimal.Decimal(\"15\"), symbol_market\n    ) == [(1, decimal.Decimal(\"61.5\")), (2, decimal.Decimal(\"61.5\"))]\n    assert consumer._split_entry_quantity(\n        decimal.Decimal(\"123\"), 3, decimal.Decimal(\"12\"), decimal.Decimal(\"15\"), symbol_market\n    ) == [(1, decimal.Decimal(\"41\")), (2, decimal.Decimal(\"41\")), (3, decimal.Decimal(\"41\"))]\n    # not enough for 3 orders, do 1\n    assert consumer._split_entry_quantity(\n        decimal.Decimal(\"0.0001\"), 3, decimal.Decimal(\"12\"), decimal.Decimal(\"15\"), symbol_market\n    ) == [(1, decimal.Decimal('0.0001'))]\n    # not enough for 3 orders, do 0\n    assert consumer._split_entry_quantity(\n        decimal.Decimal(\"0.000001\"), 3, decimal.Decimal(\"12\"), decimal.Decimal(\"15\"), symbol_market\n    ) == []\n\n\nasync def test_create_entry_with_chained_exit_orders(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.stop_loss_price_multiplier = decimal.Decimal(\"0.12\")\n    mode.exit_limit_orders_price_multiplier = decimal.Decimal(\"0.07\")\n    mode.secondary_exit_orders_price_multiplier = decimal.Decimal(\"0.035\")\n    mode.secondary_exit_orders_count = 0\n    symbol = mode.symbol\n    symbol_market = trader.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    entry_price = decimal.Decimal(\"1222\")\n    entry_order = trading_personal_data.create_order_instance(\n        trader=trader,\n        order_type=trading_enums.TraderOrderType.BUY_LIMIT,\n        symbol=symbol,\n        current_price=entry_price,\n        quantity=decimal.Decimal(\"3\"),\n        price=entry_price\n    )\n    with mock.patch.object(mode, \"create_order\", mock.AsyncMock(side_effect=lambda *args, **kwargs: args[0])) \\\n            as create_order_mock:\n        # no chained stop loss\n        # no take profit\n        mode.use_stop_loss = False\n        mode.use_take_profit_exit_orders = False\n        await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        assert entry_order.chained_orders == []\n        # reset values\n        create_order_mock.reset_mock()\n        entry_order.chained_orders = []\n\n        # chained stop loss\n        # no take profit\n        # with dependencies\n        mode.use_stop_loss = True\n        mode.use_take_profit_exit_orders = False\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, dependencies)\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=dependencies)\n        assert len(entry_order.chained_orders) == 1\n        stop_loss = entry_order.chained_orders[0]\n        assert isinstance(stop_loss, trading_personal_data.StopLossOrder)\n        assert isinstance(stop_loss.state, trading_personal_data.PendingCreationChainedOrderState)\n        assert stop_loss.symbol == entry_order.symbol\n        assert stop_loss.origin_quantity == entry_order.origin_quantity\n        assert stop_loss.origin_price == entry_price * (1 - mode.stop_loss_price_multiplier)\n        assert stop_loss.triggered_by is entry_order\n        assert stop_loss.order_group is None\n        assert stop_loss.reduce_only is False\n        assert stop_loss.update_with_triggering_order_fees is True\n        # reset values\n        create_order_mock.reset_mock()\n        entry_order.chained_orders = []\n\n        # no chained stop loss\n        # take profit\n        mode.use_stop_loss = False\n        mode.use_take_profit_exit_orders = True\n        await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        create_order_mock.reset_mock()\n        assert len(entry_order.chained_orders) == 1\n        take_profit = entry_order.chained_orders[0]\n        assert isinstance(take_profit, trading_personal_data.SellLimitOrder)\n        assert isinstance(take_profit.state, trading_personal_data.PendingCreationChainedOrderState)\n        assert take_profit.symbol == entry_order.symbol\n        assert take_profit.origin_quantity == entry_order.origin_quantity\n        assert take_profit.origin_price == entry_price * (1 + mode.exit_limit_orders_price_multiplier)\n        assert take_profit.triggered_by is entry_order\n        assert take_profit.order_group is None\n        assert take_profit.reduce_only is False\n        assert take_profit.update_with_triggering_order_fees is True\n        # reset values\n        create_order_mock.reset_mock()\n        entry_order.chained_orders = []\n\n        # chained stop loss\n        # take profit\n        mode.use_stop_loss = True\n        mode.use_take_profit_exit_orders = True\n        await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        create_order_mock.reset_mock()\n        assert len(entry_order.chained_orders) == 2\n        stop_loss = entry_order.chained_orders[0]\n        take_profit = entry_order.chained_orders[1]\n        assert stop_loss.origin_quantity == entry_order.origin_quantity\n        assert take_profit.origin_quantity == entry_order.origin_quantity\n        assert isinstance(stop_loss, trading_personal_data.StopLossOrder)\n        assert isinstance(stop_loss.state, trading_personal_data.PendingCreationChainedOrderState)\n        assert isinstance(take_profit, trading_personal_data.SellLimitOrder)\n        assert isinstance(take_profit.state, trading_personal_data.PendingCreationChainedOrderState)\n        assert take_profit.order_group is stop_loss.order_group\n        assert isinstance(take_profit.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n        assert take_profit.update_with_triggering_order_fees is True\n        assert take_profit.is_active\n        assert stop_loss.is_active\n        # reset values\n        create_order_mock.reset_mock()\n        entry_order.chained_orders = []\n\n        # with inactive orders\n        # chained stop loss\n        # 3 take profit (initial + 2 additional)\n        trader.enable_inactive_orders = True\n        mode.use_stop_loss = True\n        mode.use_take_profit_exit_orders = True\n        mode.use_secondary_exit_orders = True\n        mode.secondary_exit_orders_count = 2\n        await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        create_order_mock.reset_mock()\n        assert len(entry_order.chained_orders) == 2 * 3  # 3 stop loss & take profits couples\n        stop_losses = [\n            order\n            for order in entry_order.chained_orders\n            if isinstance(order, trading_personal_data.StopLossOrder)\n        ]\n        take_profits = [\n            order\n            for order in entry_order.chained_orders\n            if isinstance(order, trading_personal_data.SellLimitOrder)\n        ]\n        # ensure only stop losses and take profits in chained orders\n        assert len(entry_order.chained_orders) == len(stop_losses) + len(take_profits)\n        total_stop_quantity = trading_constants.ZERO\n        total_tp_quantity = trading_constants.ZERO\n        previous_stop_price = entry_price\n        previous_tp_price = trading_constants.ZERO\n        for i, (stop_loss, take_profit) in enumerate(zip(stop_losses, take_profits)):\n            is_last = i == len(stop_losses) - 1\n            assert isinstance(stop_loss.state, trading_personal_data.PendingCreationChainedOrderState)\n            assert isinstance(take_profit.state, trading_personal_data.PendingCreationChainedOrderState)\n            total_tp_quantity += take_profit.origin_quantity\n            total_stop_quantity += stop_loss.origin_quantity\n            # constant price with stop losses\n            if not previous_tp_price:\n                previous_stop_price = stop_loss.origin_price\n            else:\n                assert stop_loss.origin_price == previous_stop_price\n            # increasing price with take profits\n            assert take_profit.origin_price > previous_tp_price\n            previous_tp_price = take_profit.origin_price\n            # ensure orders are grouped together\n            assert take_profit.order_group is stop_loss.order_group\n            assert isinstance(take_profit.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n            assert stop_loss.update_with_triggering_order_fees is is_last\n            assert take_profit.update_with_triggering_order_fees is is_last\n            assert take_profit.is_active is False   # TP are inactive\n            assert stop_loss.is_active is True  # SL are active\n        # ensure selling the total entry quantity\n        assert total_stop_quantity == entry_order.origin_quantity\n        assert total_tp_quantity == entry_order.origin_quantity\n        # reset values\n        create_order_mock.reset_mock()\n        entry_order.chained_orders = []\n\n        # chained stop loss on futures\n        consumer.exchange_manager.is_future = True\n        # 3 take profit (initial + 2 additional)\n        mode.use_stop_loss = True\n        mode.use_take_profit_exit_orders = True\n        # disable use_secondary_exit_orders\n        mode.use_secondary_exit_orders = False\n        mode.secondary_exit_orders_count = 2  # disabled\n        await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        create_order_mock.reset_mock()\n        assert len(entry_order.chained_orders) == 2  # 1 take profit and one stop loss: no secondary exit is allowed\n        stop_losses = [\n            order\n            for order in entry_order.chained_orders\n            if isinstance(order, trading_personal_data.StopLossOrder)\n        ]\n        take_profits = [\n            order\n            for order in entry_order.chained_orders\n            if isinstance(order, trading_personal_data.SellLimitOrder)\n        ]\n        # ensure only stop losses and take profits in chained orders\n        assert len(stop_losses) == 1\n        assert len(take_profits) == 1\n        assert all(o.is_active is True for o in entry_order.chained_orders) # on futures, all orders are active\n        assert all(order.reduce_only is True for order in entry_order.chained_orders)   # futures: use reduce only\n        assert stop_losses[0].origin_quantity == take_profits[0].origin_quantity == entry_order.origin_quantity\n        # update_with_triggering_order_fees is false because we are trading futures\n        assert stop_losses[0].update_with_triggering_order_fees == take_profits[0].update_with_triggering_order_fees == False\n\n\nasync def test_skip_create_entry_order_when_too_many_live_exit_orders(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.stop_loss_price_multiplier = decimal.Decimal(\"0.12\")\n    mode.exit_limit_orders_price_multiplier = decimal.Decimal(\"0.07\")\n    mode.secondary_exit_orders_price_multiplier = decimal.Decimal(\"0.035\")\n    mode.secondary_exit_orders_count = 0\n    symbol = mode.symbol\n    symbol_market = trader.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    entry_price = decimal.Decimal(\"1222\")\n    entry_order = trading_personal_data.create_order_instance(\n        trader=trader,\n        order_type=trading_enums.TraderOrderType.BUY_LIMIT,\n        symbol=symbol,\n        current_price=entry_price,\n        quantity=decimal.Decimal(\"3\"),\n        price=entry_price\n    )\n    with mock.patch.object(mode, \"create_order\", mock.AsyncMock(side_effect=lambda *args, **kwargs: args[0])) \\\n            as create_order_mock, \\\n        mock.patch.object(trader.exchange_manager.exchange, \"get_max_orders_count\", mock.Mock(return_value=1)) \\\n            as get_max_orders_count_mock, \\\n        mock.patch.object(\n            trader.exchange_manager.exchange_personal_data.orders_manager, \"get_open_orders\",\n            mock.Mock(return_value=[mock.Mock(order_type=trading_enums.TraderOrderType.BUY_LIMIT)])\n        ) as get_open_orders_mock:\n        # 1.A can't even create entry limit order\n        mode.use_stop_loss = False\n        mode.use_take_profit_exit_orders = False\n        with pytest.raises(octobot_trading.errors.MaxOpenOrderReachedForSymbolError):\n            await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        assert get_max_orders_count_mock.call_count == 1 * 2   # entry: 1, stop: 0, tp: 0, 2 calls for each\n        get_max_orders_count_mock.reset_mock()\n        create_order_mock.assert_not_called()\n        get_open_orders_mock.assert_called_once()\n\n        # 1.B can create entry market order\n        entry_order.order_type=trading_enums.TraderOrderType.BUY_MARKET\n        assert await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        get_max_orders_count_mock.assert_not_called()   # not called for entry marker orders\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        create_order_mock.reset_mock()\n        assert len(entry_order.chained_orders) == 0\n        get_max_orders_count_mock.reset_mock()\n        create_order_mock.assert_not_called()\n        get_open_orders_mock.assert_called_once()\n\n        # restore order type\n        entry_order.order_type=trading_enums.TraderOrderType.BUY_LIMIT\n\n    with mock.patch.object(mode, \"create_order\", mock.AsyncMock(side_effect=lambda *args, **kwargs: args[0])) \\\n            as create_order_mock, \\\n        mock.patch.object(trader.exchange_manager.exchange, \"get_max_orders_count\", mock.Mock(return_value=1)) \\\n            as get_max_orders_count_mock:\n        mode.use_stop_loss = True\n        mode.use_take_profit_exit_orders = True\n        # 2. chained stop loss & take profit\n        assert await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        assert get_max_orders_count_mock.call_count == 3 * 2   # entry: 1, stop: 1, tp: 1, 2 calls for each\n        get_max_orders_count_mock.reset_mock()\n        create_order_mock.assert_called_once_with(entry_order, params=None, dependencies=None)\n        create_order_mock.reset_mock()\n        assert len(entry_order.chained_orders) == 2\n        stop_loss = entry_order.chained_orders[0]\n        take_profit = entry_order.chained_orders[1]\n        assert isinstance(stop_loss, trading_personal_data.StopLossOrder)\n        assert isinstance(take_profit, trading_personal_data.SellLimitOrder)\n\n        # 3. chained stop loss & take profit: impossible: cancel whole entry\n        mode.use_secondary_exit_orders = True\n        mode.secondary_exit_orders_count = 1\n        with pytest.raises(octobot_trading.errors.MaxOpenOrderReachedForSymbolError):\n            await consumer._create_entry_with_chained_exit_orders(entry_order, entry_price, symbol_market, None)\n        assert get_max_orders_count_mock.call_count == 4 * 2   # entry: 1, stop: 1, tp: 2, 2 calls for each\n        get_max_orders_count_mock.reset_mock()\n        create_order_mock.assert_not_called()\n\n\nasync def test_create_entry_order(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    symbol = mode.symbol\n    symbol_market = trader.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    price = decimal.Decimal(\"1222\")\n    order_type = trading_enums.TraderOrderType.BUY_LIMIT\n    quantity = decimal.Decimal(\"42\")\n    current_price = decimal.Decimal(\"22222\")\n    with mock.patch.object(\n            consumer, \"_create_entry_with_chained_exit_orders\", mock.AsyncMock(return_value=None)\n    ) as _create_entry_with_chained_exit_orders_mock:\n        created_orders = []\n        assert await consumer._create_entry_order(\n            order_type, quantity, price, symbol_market, symbol, created_orders, current_price, dependencies=None\n        ) is False\n        _create_entry_with_chained_exit_orders_mock.assert_called_once()\n        assert _create_entry_with_chained_exit_orders_mock.mock_calls[0].args[3] == None\n        assert created_orders == []\n    with mock.patch.object(\n        consumer, \"_create_entry_with_chained_exit_orders\", mock.AsyncMock(\n            side_effect=octobot_trading.errors.MaxOpenOrderReachedForSymbolError\n        )\n    ) as _create_entry_with_chained_exit_orders_mock:\n        created_orders = []\n        assert await consumer._create_entry_order(\n            order_type, quantity, price, symbol_market, symbol, created_orders, current_price, dependencies=None\n        ) is False\n        _create_entry_with_chained_exit_orders_mock.assert_called_once()\n        assert _create_entry_with_chained_exit_orders_mock.mock_calls[0].args[3] == None\n        assert created_orders == []\n    with mock.patch.object(\n            consumer, \"_create_entry_with_chained_exit_orders\", mock.AsyncMock(return_value=\"created_order\")\n    ) as _create_entry_with_chained_exit_orders_mock:\n        created_orders = []\n        assert await consumer._create_entry_order(\n            order_type, quantity, price, symbol_market, symbol, created_orders, current_price, dependencies=None\n        ) is True\n        _create_entry_with_chained_exit_orders_mock.assert_called_once()\n        created_order = _create_entry_with_chained_exit_orders_mock.mock_calls[0].args[0]\n        assert created_order.order_type == order_type\n        assert created_order.origin_quantity == quantity\n        assert created_order.origin_price == price\n        assert created_order.symbol == symbol\n        assert created_order.created_last_price == current_price\n        assert created_orders == [\"created_order\"]\n\n\nasync def test_create_entry_order_with_max_ratio(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    symbol = mode.symbol\n    symbol_market = trader.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n    price = decimal.Decimal(\"1222\")\n    order_type = trading_enums.TraderOrderType.BUY_LIMIT\n    quantity = decimal.Decimal(\"42\")\n    current_price = decimal.Decimal(\"22222\")\n    created_orders = []\n    with mock.patch.object(\n            consumer, \"_create_entry_with_chained_exit_orders\", mock.AsyncMock(return_value=\"created_order\")\n    ) as _create_entry_with_chained_exit_orders_mock:\n        with mock.patch.object(\n                consumer, \"_is_max_asset_ratio_reached\", mock.Mock(return_value=True)\n        ) as _is_max_asset_ratio_reached_mock:\n            dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            assert await consumer._create_entry_order(\n                order_type, quantity, price, symbol_market, symbol, created_orders, current_price, dependencies=dependencies\n            ) is False\n            _is_max_asset_ratio_reached_mock.assert_called_with(symbol)\n            _create_entry_with_chained_exit_orders_mock.assert_not_called()\n        with mock.patch.object(\n                consumer, \"_is_max_asset_ratio_reached\", mock.Mock(return_value=False)\n        ) as _is_max_asset_ratio_reached_mock:\n            dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            assert await consumer._create_entry_order(\n                order_type, quantity, price, symbol_market, symbol, created_orders, current_price, dependencies=dependencies\n            ) is True\n            _is_max_asset_ratio_reached_mock.assert_called_with(symbol)\n            _create_entry_with_chained_exit_orders_mock.assert_called_once()\n            assert _create_entry_with_chained_exit_orders_mock.mock_calls[0].args[3] == dependencies\n\n\nasync def test_create_create_order_if_possible_with_funds_already_locked(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    symbol = mode.symbol\n    with mock.patch.object(\n        consumer, \"create_new_orders\", mock.AsyncMock(return_value=[\"orders\"])\n    ) as create_new_orders_mock:\n        # case 1: all OK => enough available funds\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].available = \\\n            decimal.Decimal(\"1000\")\n        # DOES NOT cancel orders before creating entries: can new entries as funds are available\n        mode.cancel_open_orders_at_each_entry = False\n        assert await consumer.create_order_if_possible(symbol, None, trading_enums.EvaluatorStates.VERY_LONG.value) == [\"orders\"]\n        create_new_orders_mock.assert_called_once()\n        create_new_orders_mock.reset_mock()\n\n        # DOES cancel orders before creating entries: can create new entries as funds are available\n        mode.cancel_open_orders_at_each_entry = True\n        assert await consumer.create_order_if_possible(symbol, None, trading_enums.EvaluatorStates.VERY_LONG.value) == [\"orders\"]\n        create_new_orders_mock.assert_called_once()\n        create_new_orders_mock.reset_mock()\n\n        # case 2: NOT all OK => not enough available funds\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].available = \\\n            decimal.Decimal(\"0\")\n        # DOES NOT cancel orders before creating entries: can't create new entries when no funds are available\n        mode.cancel_open_orders_at_each_entry = False\n        assert await consumer.create_order_if_possible(symbol, None, trading_enums.EvaluatorStates.VERY_LONG.value) == []\n        create_new_orders_mock.assert_not_called()\n\n        # DOES cancel orders before creating entries: can create new entries when no funds are available\n        mode.cancel_open_orders_at_each_entry = True\n        assert await consumer.create_order_if_possible(symbol, None, trading_enums.EvaluatorStates.VERY_LONG.value) == [\"orders\"]\n        create_new_orders_mock.assert_called_once()\n\n\nasync def test_is_max_asset_ratio_reached(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    assert mode.max_asset_holding_ratio == trading_constants.ONE\n    symbol = \"BTC/USDT\"\n    base = \"BTC\"\n    portfolio_value_holder = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder\n    with mock.patch.object(\n            portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(return_value=decimal.Decimal(\"1\"))\n    ) as get_holdings_ratio_mock:\n        assert consumer._is_max_asset_ratio_reached(symbol) is True\n        get_holdings_ratio_mock.assert_called_with(base, include_assets_in_open_orders=True)\n    with mock.patch.object(\n            portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(return_value=decimal.Decimal(\"0.4\"))\n    ) as get_holdings_ratio_mock:\n        assert consumer._is_max_asset_ratio_reached(symbol) is False\n        get_holdings_ratio_mock.assert_called_with(base, include_assets_in_open_orders=True)\n        get_holdings_ratio_mock.reset_mock()\n\n        mode.max_asset_holding_ratio = decimal.Decimal(\"0.4\")\n        assert consumer._is_max_asset_ratio_reached(symbol) is True\n        get_holdings_ratio_mock.assert_called_with(base, include_assets_in_open_orders=True)\n        get_holdings_ratio_mock.reset_mock()\n\n        mode.max_asset_holding_ratio = decimal.Decimal(\"0.41\")\n        assert consumer._is_max_asset_ratio_reached(symbol) is False\n        get_holdings_ratio_mock.assert_called_with(base, include_assets_in_open_orders=True)\n        get_holdings_ratio_mock.reset_mock()\n\n        # disabled on futures\n        consumer.exchange_manager.is_future = True\n        assert consumer._is_max_asset_ratio_reached(symbol) is False\n        get_holdings_ratio_mock.assert_not_called()\n\n\nasync def test_create_new_orders(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.secondary_entry_orders_count = 0\n    symbol = mode.symbol\n\n    def _create_basic_order(side, quantity=decimal.Decimal(\"0.1\")):\n        created_order = trading_personal_data.Order(trader)\n        created_order.symbol = symbol\n        created_order.side = side\n        created_order.origin_quantity = quantity\n        created_order.origin_price = decimal.Decimal(\"1000\")\n        return created_order\n\n    async def _create_entry_order(_, __, ___, ____, _____, created_orders, ______, dependencies):\n        created_order = _create_basic_order(trading_enums.TradeOrderSide.BUY)\n        created_orders.append(created_order)\n        return bool(created_order)\n\n    with mock.patch.object(\n            consumer, \"_create_entry_order\", mock.AsyncMock(side_effect=_create_entry_order)\n    ) as _create_entry_order_mock, mock.patch.object(\n        mode, \"cancel_order\", mock.AsyncMock(return_value=(True, trading_signals.get_order_dependency(mock.Mock(order_id=\"456\"))))\n    ) as cancel_order_mock:\n        # neutral state\n        assert await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.NEUTRAL.value) == []\n        cancel_order_mock.assert_not_called()\n        _create_entry_order_mock.assert_not_called()\n        # no configured amount\n        mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"\"\n        assert await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value) == []\n        cancel_order_mock.assert_not_called()\n        _create_entry_order_mock.assert_not_called()\n        # no configured secondary amount\n        mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"12%\"\n        mode.secondary_entry_orders_amount = \"\"\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value, dependencies=dependencies)\n        cancel_order_mock.assert_not_called()\n        _create_entry_order_mock.assert_called_once()\n        assert _create_entry_order_mock.mock_calls[0].args[7] == dependencies\n        _create_entry_order_mock.reset_mock()\n\n        # with secondary orders but no configured secondary amount\n        mode.secondary_entry_orders_count = 4\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value)\n        cancel_order_mock.assert_not_called()\n        # only called once: missing secondary quantity prevents secondary orders creation\n        _create_entry_order_mock.assert_called_once()\n        _create_entry_order_mock.reset_mock()\n\n        mode.use_market_entry_orders = False\n        mode.use_secondary_entry_orders = True\n        mode.secondary_entry_orders_amount = \"20q\"\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value)\n        cancel_order_mock.assert_not_called()\n        # called as many times as there are orders to create\n        assert _create_entry_order_mock.call_count == 1 + 4\n        # ensure each secondary order has a lower price\n        previous_price = None\n        for i, call in enumerate(_create_entry_order_mock.mock_calls):\n            if i == 0:\n                assert call.args[1] == decimal.Decimal('0.24')  # initial quantity\n            else:\n                assert call.args[1] == decimal.Decimal('0.02')  # secondary quantity\n            assert call.args[0] is trading_enums.TraderOrderType.BUY_LIMIT\n            call_price = call.args[2]\n            if previous_price is None:\n                previous_price = call_price\n            else:\n                assert call_price < previous_price\n        _create_entry_order_mock.reset_mock()\n\n        mode.use_market_entry_orders = True\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.VERY_LONG.value)\n        cancel_order_mock.assert_not_called()\n        # called as many times as there are orders to create\n        assert _create_entry_order_mock.call_count == 1 + 4\n        for i, call in enumerate(_create_entry_order_mock.mock_calls):\n            expected_type = trading_enums.TraderOrderType.BUY_MARKET \\\n                if i == 0 else trading_enums.TraderOrderType.BUY_LIMIT\n            assert call.args[0] is expected_type\n        _create_entry_order_mock.reset_mock()\n\n        # with existing orders locking all funds: cancel them all\n        existing_orders = [\n            _create_basic_order(trading_enums.TradeOrderSide.BUY, decimal.Decimal(1)),\n            _create_basic_order(trading_enums.TradeOrderSide.BUY, decimal.Decimal(1)),\n            _create_basic_order(trading_enums.TradeOrderSide.SELL),\n        ]\n        for order in existing_orders:\n            await trader.exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(order)\n\n        assert trader.exchange_manager.exchange_personal_data.orders_manager.get_all_orders(symbol=symbol) == \\\n               existing_orders\n        # cancelled orders amounts are taken into account to consider entry orders creatable\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].available = \\\n            decimal.Decimal(\"0\")\n\n        async def _cancel_order(order, dependencies=None):\n            trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].available += \\\n                order.origin_quantity * order.origin_price\n            return True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456\")])\n\n        # with existing orders locking all funds: cancel non partially filled ones\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        with mock.patch.object(\n            mode, \"cancel_order\", mock.AsyncMock(side_effect=_cancel_order)\n        ) as cancel_order_mock_2:\n            with mock.patch.object(\n                trader.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol)[0], \"is_partially_filled\", mock.Mock(return_value=True)\n            ) as is_partially_filled_mock:\n                await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value, dependencies=dependencies)\n                is_partially_filled_mock.assert_called_once()\n                assert cancel_order_mock_2.call_count == 1\n                assert cancel_order_mock_2.mock_calls[0].args[0] == existing_orders[1]\n                assert cancel_order_mock_2.mock_calls[0].kwargs[\"dependencies\"] == dependencies\n                cancel_order_mock_2.reset_mock()\n                # called as many times as there are orders to create\n                assert _create_entry_order_mock.call_count == 1 + 4\n                assert _create_entry_order_mock.mock_calls[0].args[7] == trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456\")])\n                _create_entry_order_mock.reset_mock()\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].available = \\\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].total\n\n        # order 2 is now partially filled, it won't be cancelled\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        with mock.patch.object(\n            mode, \"cancel_order\", mock.AsyncMock(side_effect=_cancel_order)\n        ) as cancel_order_mock_2:\n            await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value, dependencies=dependencies)\n            assert cancel_order_mock_2.call_count == 2\n            assert cancel_order_mock_2.mock_calls[0].args[0] == existing_orders[0]\n            assert cancel_order_mock_2.mock_calls[1].args[0] == existing_orders[1]\n            assert cancel_order_mock_2.mock_calls[0].kwargs[\"dependencies\"] == dependencies\n            assert cancel_order_mock_2.mock_calls[1].kwargs[\"dependencies\"] == dependencies\n            cancel_order_mock_2.reset_mock()\n            # called as many times as there are orders to create\n            assert _create_entry_order_mock.call_count == 1 + 4\n            assert _create_entry_order_mock.mock_calls[0].args[7] == trading_signals.get_orders_dependencies([\n                mock.Mock(order_id=\"456\"), mock.Mock(order_id=\"456\")]\n            )\n            _create_entry_order_mock.reset_mock()\n            trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].available = \\\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USDT\"].total\n\n        # without enough funds to create every secondary order\n        mode.secondary_entry_orders_count = 30  # can't create 30 orders, each using 100 USD of available funds\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value, dependencies=None)\n        assert cancel_order_mock.call_count == 2  # still cancel open orders\n        assert cancel_order_mock.mock_calls[0].args[0] == existing_orders[0]\n        assert cancel_order_mock.mock_calls[1].args[0] == existing_orders[1]\n        portfolio = trading_api.get_portfolio(trader.exchange_manager)\n        order_example = _create_basic_order(trading_enums.TradeOrderSide.BUY)\n        # ensure used all funds\n        assert portfolio[\"USDT\"].available / _create_entry_order_mock.call_count == \\\n               order_example.origin_quantity * order_example.origin_price\n        cancel_order_mock.reset_mock()\n        # called as many times as there are orders to create\n        # 10 orders out of 30 got skipped\n        assert _create_entry_order_mock.call_count == 1 + 19\n        _create_entry_order_mock.reset_mock()\n\n        with mock.patch.object(consumer, \"_is_max_asset_ratio_reached\", mock.Mock(return_value=True)) as \\\n            _is_max_asset_ratio_reached_mock:\n\n            async def _create_entry_order_2(_, __, ___, ____, _____, created_orders, ______, dependencies):\n                created_order = _create_basic_order(trading_enums.TradeOrderSide.BUY)\n                created_orders.append(created_order)\n                # simulate no order created\n                return False\n            with mock.patch.object(consumer, \"_create_entry_order\", mock.Mock(side_effect=_create_entry_order_2)) as \\\n                 _create_entry_order_mock_2:\n                # without enough funds to create every secondary order\n                mode.secondary_entry_orders_count = 30  # can't create 30 orders, each using 100 USD of available funds\n                await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value)\n                assert cancel_order_mock.call_count == 2  # still cancel open orders\n                assert cancel_order_mock.mock_calls[0].args[0] == existing_orders[0]\n                assert cancel_order_mock.mock_calls[1].args[0] == existing_orders[1]\n                portfolio = trading_api.get_portfolio(trader.exchange_manager)\n                order_example = _create_basic_order(trading_enums.TradeOrderSide.BUY)\n                # did NOT ensure used all funds as only 1 secondary order is created\n                # (_create_entry_order returned False)\n                assert portfolio[\"USDT\"].available / _create_entry_order_mock_2.call_count > \\\n                       order_example.origin_quantity * order_example.origin_price\n                cancel_order_mock.reset_mock()\n                # called as many times as there are orders to create\n                # 1 orders out of 30 got created\n                assert _create_entry_order_mock_2.call_count == 1 + 1\n                _create_entry_order_mock_2.reset_mock()\n                # doesn't matter if _is_max_asset_ratio_reached returns True: orders are still cancelled\n                _is_max_asset_ratio_reached_mock.assert_not_called()\n\n        # invalid initial orders according to exchange rules: does not cancel existing orders\n        mode.secondary_entry_orders_count = 0\n        mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"0.000001q\"\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value)\n        # orders are not cancelled\n        cancel_order_mock.assert_not_called()\n        # still called once in case orders can be created anyway\n        _create_entry_order_mock.assert_called_once()\n        _create_entry_order_mock.reset_mock()\n\n        # invalid secondary orders according to exchange rules: does not cancel existing orders\n        mode.secondary_entry_orders_count = 1\n        mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"20q\"\n        mode.secondary_entry_orders_amount = \"0.000001q\"\n        await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value)\n        # orders are not cancelled\n        cancel_order_mock.assert_not_called()\n        # still called once for initial entry and once for secondary in case orders can be created anyway\n        assert _create_entry_order_mock.call_count == 2\n        _create_entry_order_mock.reset_mock()\n\n        with mock.patch.object(\n            trading_personal_data, \"decimal_check_and_adapt_order_details_if_necessary\", mock.Mock(return_value=[])\n        ) as decimal_check_and_adapt_order_details_if_necessary_mock:\n            # without enough funds to create initial orders according to exchange rules: does not cancel existing orders\n            mode.secondary_entry_orders_count = 0\n            mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"0.20q\"\n            await consumer.create_new_orders(symbol, None, trading_enums.EvaluatorStates.LONG.value)\n            # orders are not cancelled\n            cancel_order_mock.assert_not_called()\n            # still called once in case orders can be created anyway\n            _create_entry_order_mock.assert_called_once()\n            decimal_check_and_adapt_order_details_if_necessary_mock.assert_called_once()\n\n\nasync def test_create_new_orders_fully_used_portfolio(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.use_secondary_entry_orders = True\n    mode.secondary_entry_orders_count = 1\n    mode.secondary_entry_orders_amount = \"8%t\"\n    mode.use_market_entry_orders = False\n    mode.cancel_open_orders_at_each_entry = False\n    mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"8%t\"\n\n    mode.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(\"DOGE/USDT\"),\n        commons_symbols.parse_symbol(\"LINK/USDT\")\n    ]\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n    portfolio[\"USDT\"].available = decimal.Decimal(\"79.98463886\")\n    portfolio[\"USDT\"].total = decimal.Decimal(\"1000\")\n    portfolio.pop(\"USD\", None)\n    portfolio.pop(\"BTC\", None)\n\n    trading_api.force_set_mark_price(trader.exchange_manager, \"DOGE/USDT\", 0.06852)\n    trading_api.force_set_mark_price(trader.exchange_manager, \"LINK/USDT\", 11.0096)\n    converter = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter\n    converter.update_last_price(\"DOGE/USDT\", decimal.Decimal(\"0.06852\"))\n    converter.update_last_price(\"LINK/USDT\", decimal.Decimal(\"11.0096\"))\n\n    def _get_market_status(symbol, **kwargs):\n        # example from kucoin on 1st nov 2023\n        if symbol == \"DOGE/USDT\":\n            return {\n                'limits': {\n                    'amount': {'max': 10000000000.0, 'min': 10.0},\n                    'cost': {'max': 99999999.0, 'min': 0.1},\n                    'leverage': {'max': None, 'min': None},\n                    'price': {'max': None, 'min': None}\n                },\n                'precision': {'amount': 4, 'price': 5}\n            }\n        if symbol == \"LINK/USDT\":\n            return {\n                'limits': {\n                    'amount': {'max': 10000000000.0, 'min': 0.001},\n                    'cost': {'max': 99999999.0, 'min': 0.1},\n                    'leverage': {'max': None, 'min': None},\n                    'price': {'max': None, 'min': None}\n                },\n                'precision': {'amount': 4, 'price': 4}\n            }\n\n    async def _create_order(order, **kwargs):\n        await order.initialize(is_from_exchange_data=True, enable_associated_orders_creation=False)\n        return order\n\n    with mock.patch.object(\n            trader.exchange_manager.exchange, \"get_market_status\", mock.Mock(side_effect=_get_market_status)\n    ) as get_market_status_mock, mock.patch.object(\n            mode, \"create_order\", mock.AsyncMock(side_effect=_create_order)\n    ) as create_order_mock:\n        orders_1, orders_2 = await asyncio.gather(\n            consumer.create_new_orders(\"DOGE/USDT\", None, trading_enums.EvaluatorStates.LONG.value),\n            consumer.create_new_orders(\"LINK/USDT\", None, trading_enums.EvaluatorStates.LONG.value),\n        )\n        assert orders_1\n        assert len(orders_1) == 1\n        get_market_status_mock.reset_mock()\n        assert orders_2\n        assert len(orders_2) == 1\n\n        total_cost = orders_1[0].total_cost + orders_2[0].total_cost\n        assert total_cost <= decimal.Decimal(\"79.98463886\")\n\n\nasync def test_create_new_buy_orders_fees_in_quote(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.use_secondary_entry_orders = True\n    mode.secondary_entry_orders_count = 1\n    mode.secondary_entry_orders_amount = \"8%t\"\n    mode.use_market_entry_orders = False\n    mode.cancel_open_orders_at_each_entry = False\n    mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"8%t\"\n\n    mode.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(\"DOGE/USDT\"),\n        commons_symbols.parse_symbol(\"LINK/USDT\")\n    ]\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n    portfolio[\"USDT\"].available = decimal.Decimal(\"279.98463886\")\n    portfolio[\"USDT\"].total = decimal.Decimal(\"1000\")\n    portfolio.pop(\"USD\", None)\n    portfolio.pop(\"BTC\", None)\n\n    trading_api.force_set_mark_price(trader.exchange_manager, \"DOGE/USDT\", 0.06852)\n    trading_api.force_set_mark_price(trader.exchange_manager, \"LINK/USDT\", 11.0096)\n    converter = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter\n    converter.update_last_price(\"DOGE/USDT\", decimal.Decimal(\"0.06852\"))\n    converter.update_last_price(\"LINK/USDT\", decimal.Decimal(\"11.0096\"))\n\n\n    def _get_fees_currency(base, quote, order_type):\n        # force quote fees\n        return quote\n\n    def _read_fees_from_config(fees):\n        # use 20% fees\n        fees[trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value] = 0.2\n        fees[trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value] = 0.2\n        fees[trading_enums.ExchangeConstantsMarketPropertyColumns.FEE.value] = 0.2\n\n    async def _create_order(order, **kwargs):\n        await order.initialize(is_from_exchange_data=True, enable_associated_orders_creation=False)\n        return order\n\n    with mock.patch.object(\n        mode, \"create_order\", mock.AsyncMock(side_effect=_create_order)\n    ) as create_order_mock, mock.patch.object(\n        trader.exchange_manager.exchange.connector, \"_get_fees_currency\",\n        mock.Mock(side_effect=_get_fees_currency)\n    ) as _get_fees_currency_mock, mock.patch.object(\n        trader.exchange_manager.exchange.connector, \"_read_fees_from_config\",\n        mock.Mock(side_effect=_read_fees_from_config)\n    ) as _get_fees_currency_mock:\n        orders_1, orders_2 = await asyncio.gather(\n            consumer.create_new_orders(\"DOGE/USDT\", None, trading_enums.EvaluatorStates.LONG.value),\n            consumer.create_new_orders(\"LINK/USDT\", None, trading_enums.EvaluatorStates.LONG.value),\n        )\n        assert orders_1\n        assert len(orders_1) == 2\n        assert orders_2\n        assert len(orders_2) == 1   # secondary order skipped because not enough funds after fees account\n\n        total_cost = orders_1[0].total_cost + orders_1[1].total_cost + orders_2[0].total_cost\n        assert total_cost <= decimal.Decimal(\"225.98463886\")  # took fees into account\n\n\nasync def test_create_new_buy_orders_futures_trading(futures_tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(futures_tools, _get_config(futures_tools, update))\n    mode.use_secondary_entry_orders = True\n    mode.secondary_entry_orders_count = 3\n    mode.secondary_entry_orders_amount = \"8%t\"\n    mode.use_market_entry_orders = False\n    mode.cancel_open_orders_at_each_entry = False\n    mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"8%t\"\n\n    mode.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(\"BTC/USDT:USDT\")\n    ]\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n    portfolio[\"USDT\"].available = decimal.Decimal(\"200\")\n    portfolio[\"USDT\"].total = decimal.Decimal(\"200\")\n    portfolio.pop(\"USD\", None)\n    portfolio.pop(\"BTC\", None)\n\n    trading_api.force_set_mark_price(trader.exchange_manager, \"BTC/USDT:USDT\", 11.0096)\n    converter = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter\n    converter.update_last_price(\"BTC/USDT:USDT\", decimal.Decimal(\"11.0096\"))\n\n\n    def _get_fees_currency(base, quote, order_type):\n        # force quote fees\n        return quote\n\n    def _read_fees_from_config(fees):\n        # use 0.2% fees\n        fees[trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value] = 0.002\n        fees[trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value] = 0.002\n        fees[trading_enums.ExchangeConstantsMarketPropertyColumns.FEE.value] = 0.002\n\n    async def _create_order(order, **kwargs):\n        await order.initialize(is_from_exchange_data=True, enable_associated_orders_creation=False)\n        return order\n\n    with mock.patch.object(\n        mode, \"create_order\", mock.AsyncMock(side_effect=_create_order)\n    ) as create_order_mock, mock.patch.object(\n        trader.exchange_manager.exchange.connector, \"_get_fees_currency\",\n        mock.Mock(side_effect=_get_fees_currency)\n    ) as _get_fees_currency_mock, mock.patch.object(\n        trader.exchange_manager.exchange.connector, \"_read_fees_from_config\",\n        mock.Mock(side_effect=_read_fees_from_config)\n    ) as _get_fees_currency_mock:\n        orders = await consumer.create_new_orders(\"BTC/USDT:USDT\", None, trading_enums.EvaluatorStates.LONG.value)\n        assert orders\n        assert len(orders) == 4\n        total_cost = sum(order.total_cost for order in orders)\n        assert round(total_cost) == decimal.Decimal(\"56\")\n\n\nasync def test_create_set_leverage_on_futures_trading(futures_tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(futures_tools, _get_config(futures_tools, update))\n    mode.use_secondary_entry_orders = True\n    mode.secondary_entry_orders_count = 3\n    mode.secondary_entry_orders_amount = \"8%t\"\n    mode.use_market_entry_orders = False\n    mode.cancel_open_orders_at_each_entry = False\n    mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \"8%t\"\n    with mock.patch.object(mode, \"set_leverage\", mock.AsyncMock()) as set_leverage_mock, \\\n        mock.patch.object(producer, \"submit_trading_evaluation\", mock.AsyncMock()) as submit_trading_evaluation:\n        await producer._process_entries(\"Bitcoin\", \"BTC\", trading_enums.EvaluatorStates.SHORT)\n        # nothing happens on short\n        set_leverage_mock.assert_not_called()\n        submit_trading_evaluation.assert_not_called()\n        await producer._process_entries(\"Bitcoin\", \"BTC/USDT:USDT\", trading_enums.EvaluatorStates.LONG)\n        # leverage config is not set\n        set_leverage_mock.assert_not_called()\n        submit_trading_evaluation.assert_called_once()\n        submit_trading_evaluation.reset_mock()\n        # now updated leverage\n        mode.trading_config[trading_constants.CONFIG_LEVERAGE] = 4\n        await producer._process_entries(\"Bitcoin\", \"BTC/USDT:USDT\", trading_enums.EvaluatorStates.LONG)\n        set_leverage_mock.assert_called_once_with(\n            \"BTC/USDT:USDT\", trading_enums.PositionSide.BOTH, decimal.Decimal(4)\n        )\n        submit_trading_evaluation.assert_called_once()\n        set_leverage_mock.reset_mock()\n        submit_trading_evaluation.reset_mock()\n        # don't update leverage when position already has the right leverage\n        mode.trading_config[trading_constants.CONFIG_LEVERAGE] = 1\n        await producer._process_entries(\"Bitcoin\", \"BTC/USDT:USDT\", trading_enums.EvaluatorStates.LONG)\n        set_leverage_mock.assert_not_called()\n        submit_trading_evaluation.assert_called_once()\n        set_leverage_mock.reset_mock()\n        submit_trading_evaluation.reset_mock()\n\n\nasync def test_single_exchange_process_optimize_initial_portfolio(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n\n    with mock.patch.object(\n            octobot_trading.modes, \"convert_assets_to_target_asset\", mock.AsyncMock(return_value=[\"order_1\"])\n    ) as convert_assets_to_target_asset_mock:\n        orders = await mode.single_exchange_process_optimize_initial_portfolio([\"BTC\", \"ETH\"], \"USDT\", {})\n        convert_assets_to_target_asset_mock.assert_called_once_with(mode, [\"BTC\", \"ETH\"], \"USDT\", {})\n        assert orders == [\"order_1\"]\n        convert_assets_to_target_asset_mock.reset_mock()\n\n        mode.exchange_manager.exchange_config.traded_symbols = [\n            commons_symbols.parse_symbol(\"SOL/USDT\"),\n            commons_symbols.parse_symbol(\"BCC/ATOM\"),\n        ]\n        orders = await mode.single_exchange_process_optimize_initial_portfolio([\"BTC\", \"ETH\"], \"USDT\", {})\n        convert_assets_to_target_asset_mock.assert_called_once_with(\n            mode, [\"BCC\", \"BTC\", \"ETH\", \"SOL\"], \"USDT\", {}\n        )\n        assert orders == [\"order_1\"]\n\n\nasync def test_single_exchange_process_health_check(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    exchange_manager = trader.exchange_manager\n    with mock.patch.object(producer, \"dca_task\", mock.AsyncMock()):  # prevent auto dca task\n\n        portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n        converter = trader.exchange_manager.exchange_personal_data.portfolio_manager.\\\n            portfolio_value_holder.value_converter\n        converter.update_last_price(mode.symbol, decimal.Decimal(\"1000\"))\n\n        origin_portfolio_USDT = portfolio[\"USDT\"].total\n\n        # no traded symbols: no orders\n        exchange_manager.exchange_config.traded_symbols = []\n        producer.last_activity = None\n        assert await mode.single_exchange_process_health_check([], {}) == []\n        assert portfolio[\"USDT\"].total == origin_portfolio_USDT\n        assert producer.last_activity is None\n\n        # with traded symbols: 1 order as BTC is not already in a sell order\n        exchange_manager.exchange_config.traded_symbols = [commons_symbols.parse_symbol(mode.symbol)]\n\n        # no self.use_take_profit_exit_orders or self.use_stop_loss\n        mode.use_take_profit_exit_orders = False\n        mode.use_stop_loss = False\n        assert await mode.single_exchange_process_health_check([], {}) == []\n        assert producer.last_activity is None\n\n        # no health check in backtesting\n        exchange_manager.is_backtesting = True\n        assert await mode.single_exchange_process_health_check([], {}) == []\n        assert producer.last_activity is None\n        exchange_manager.is_backtesting = False\n\n        # use_take_profit_exit_orders is True: health check can proceed\n        mode.use_take_profit_exit_orders = True\n        orders = await mode.single_exchange_process_health_check([], {})\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.CREATED_ORDERS\n        )\n        assert len(orders) == 1\n        sell_order = orders[0]\n        assert isinstance(sell_order, trading_personal_data.SellMarketOrder)\n        assert sell_order.symbol == mode.symbol\n        assert sell_order.origin_quantity == decimal.Decimal(10)\n        assert portfolio[\"BTC\"].total == trading_constants.ZERO\n        after_btc_usdt_portfolio = portfolio[\"USDT\"].total\n        assert after_btc_usdt_portfolio > origin_portfolio_USDT\n\n        # now that BTC is sold, calling it again won't create any order\n        producer.last_activity = None\n        assert await mode.single_exchange_process_health_check([], {}) == []\n        assert producer.last_activity is None\n\n        # add ETH in portfolio: will also be sold but is bellow threshold\n        converter.update_last_price(\"ETH/USDT\", decimal.Decimal(\"100\"))\n        exchange_manager.client_symbols.append(\"ETH/USDT\")\n        exchange_manager.exchange_config.traded_symbols.append(commons_symbols.parse_symbol(\"ETH/USDT\"))\n        eth_holdings = decimal.Decimal(2)\n        portfolio[\"ETH\"] = trading_personal_data.SpotAsset(\"ETH\", eth_holdings, eth_holdings)\n        producer.last_activity = None\n        assert await mode.single_exchange_process_health_check([], {}) == []\n        assert producer.last_activity is None\n\n        # more ETH: can sell but not all of it because of partially filled buy orders\n        eth_holdings = decimal.Decimal(200)\n        portfolio[\"ETH\"] = trading_personal_data.SpotAsset(\"ETH\", eth_holdings, eth_holdings)\n        producer.last_activity = None\n        buy_limit = trading_personal_data.BuyLimitOrder(trader)\n        buy_limit.symbol = \"ETH/USDT\"\n        buy_limit.origin_quantity = decimal.Decimal(200)\n        buy_limit.filled_quantity = decimal.Decimal(199)\n        with mock.patch.object(\n            exchange_manager.exchange_personal_data.orders_manager, \"get_open_orders\",\n            mock.Mock(return_value=[buy_limit])\n        ) as get_open_orders_mock:\n            assert await mode.single_exchange_process_health_check([], {}) == []\n            assert get_open_orders_mock.call_count == 2\n            assert producer.last_activity is None\n\n        # no partially filled buy orders: sell ETH\n        eth_holdings = decimal.Decimal(200)\n        portfolio[\"ETH\"] = trading_personal_data.SpotAsset(\"ETH\", eth_holdings, eth_holdings)\n        producer.last_activity = None\n        orders = await mode.single_exchange_process_health_check([], {})\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.CREATED_ORDERS\n        )\n        assert len(orders) == 1\n        sell_order = orders[0]\n        assert isinstance(sell_order, trading_personal_data.SellMarketOrder)\n        assert sell_order.symbol == \"ETH/USDT\"\n        assert sell_order.origin_quantity == eth_holdings\n        assert portfolio[\"ETH\"].total == trading_constants.ZERO\n        after_eth_usdt_portfolio = portfolio[\"USDT\"].total\n        assert after_eth_usdt_portfolio > after_btc_usdt_portfolio\n\n        # add ETH to be sold but already in sell order: do not sell the part in sell orders\n        eth_holdings = decimal.Decimal(200)\n        portfolio[\"ETH\"] = trading_personal_data.SpotAsset(\"ETH\", eth_holdings, eth_holdings)\n        existing_sell_order = trading_personal_data.SellLimitOrder(trader)\n        existing_sell_order.origin_quantity = decimal.Decimal(45)\n        existing_sell_order.symbol = \"ETH/USDT\"\n        await exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(existing_sell_order)\n        producer.last_activity = None\n        orders = await mode.single_exchange_process_health_check([], {})\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.CREATED_ORDERS\n        )\n        assert len(orders) == 1\n        sell_order = orders[0]\n        assert isinstance(sell_order, trading_personal_data.SellMarketOrder)\n        assert sell_order.symbol == \"ETH/USDT\"\n        assert sell_order.origin_quantity == eth_holdings - decimal.Decimal(45)\n        assert portfolio[\"ETH\"].total == decimal.Decimal(45)\n        after_eth_usdt_portfolio = portfolio[\"USDT\"].total\n        assert after_eth_usdt_portfolio > after_btc_usdt_portfolio\n\n        # add ETH to be sold but already in chained sell order: do not sell the part in chained sell orders\n        eth_holdings = decimal.Decimal(200)\n        portfolio[\"ETH\"] = trading_personal_data.SpotAsset(\"ETH\", eth_holdings, eth_holdings)\n        chained_sell_order = trading_personal_data.SellLimitOrder(trader)\n        chained_sell_order.origin_quantity = decimal.Decimal(10)\n        chained_sell_order.symbol = \"ETH/USDT\"\n        producer.last_activity = None\n        orders = await mode.single_exchange_process_health_check([chained_sell_order], {})\n        assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n            trading_enums.TradingModeActivityType.CREATED_ORDERS\n        )\n        assert len(orders) == 1\n        sell_order = orders[0]\n        assert isinstance(sell_order, trading_personal_data.SellMarketOrder)\n        assert sell_order.symbol == \"ETH/USDT\"\n        assert sell_order.origin_quantity == eth_holdings - decimal.Decimal(45) - decimal.Decimal(10)\n        assert portfolio[\"ETH\"].total == decimal.Decimal(45) + decimal.Decimal(10)\n        after_eth_usdt_portfolio = portfolio[\"USDT\"].total\n        assert after_eth_usdt_portfolio > after_btc_usdt_portfolio\n\n        # add ETH to be sold but already in chained sell order: do not sell the part in chained sell orders:\n        # sell orders make it bellow threshold: no market sell created\n        eth_holdings = decimal.Decimal(200)\n        portfolio[\"ETH\"] = trading_personal_data.SpotAsset(\"ETH\", eth_holdings, eth_holdings)\n        chained_sell_order = trading_personal_data.SellLimitOrder(trader)\n        chained_sell_order.origin_quantity = decimal.Decimal(55)\n        chained_sell_order.symbol = \"ETH/USDT\"\n        producer.last_activity = None\n        assert await mode.single_exchange_process_health_check([chained_sell_order], {}) == []\n        assert producer.last_activity is None\n\n\nasync def _check_open_orders_count(trader, count):\n    assert len(trading_api.get_open_orders(trader.exchange_manager)) == count\n\n\nasync def _get_tools(symbol=\"BTC/USDT\"):\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n    exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n    exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n    # use backtesting not to spam exchanges apis\n    exchange_manager.is_simulated = True\n    exchange_manager.is_backtesting = True\n    exchange_manager.use_cached_markets = False\n    backtesting = await backtesting_api.initialize_backtesting(\n        config,\n        exchange_ids=[exchange_manager.id],\n        matrix_id=None,\n        data_files=[os.path.join(test_config.TEST_CONFIG_FOLDER,\n                                 \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n    exchange_manager.exchange = exchanges.ExchangeSimulator(\n        exchange_manager.config, exchange_manager, backtesting\n    )\n    await exchange_manager.exchange.initialize()\n    for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n        await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                         exchange_manager=exchange_manager)\n\n    trader = exchanges.TraderSimulator(config, exchange_manager)\n    await trader.initialize()\n\n    mode = Mode.DCATradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    # trading mode is not initialized: to be initialized with the required config in tests\n\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n\n    # set BTC/USDT price at 1000 USDT\n    trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n    return mode, trader\n\n\nasync def _get_futures_tools(symbol=\"BTC/USDT:USDT\"):\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n    exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n    exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n    # use backtesting not to spam exchanges apis\n    exchange_manager.is_spot_only = False\n    exchange_manager.is_future = True\n    exchange_manager.is_simulated = True\n    exchange_manager.is_backtesting = True\n    exchange_manager.use_cached_markets = False\n    backtesting = await backtesting_api.initialize_backtesting(\n        config,\n        exchange_ids=[exchange_manager.id],\n        matrix_id=None,\n        data_files=[os.path.join(test_config.TEST_CONFIG_FOLDER,\n                                 \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n    exchange_manager.exchange = exchanges.ExchangeSimulator(\n        exchange_manager.config, exchange_manager, backtesting\n    )\n    await exchange_manager.exchange.initialize()\n    for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n        await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                         exchange_manager=exchange_manager)\n    contract = trading_exchange_data.FutureContract(\n        pair=symbol,\n        margin_type=trading_enums.MarginType.ISOLATED,\n        contract_type=trading_enums.FutureContractType.LINEAR_PERPETUAL,\n        current_leverage=trading_constants.ONE,\n        maximum_leverage=trading_constants.ONE_HUNDRED\n    )\n    exchange_manager.exchange.set_pair_future_contract(symbol, contract)\n    trader = exchanges.TraderSimulator(config, exchange_manager)\n    await trader.initialize()\n\n    mode = Mode.DCATradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    # trading mode is not initialized: to be initialized with the required config in tests\n\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n\n    # set BTC/USDT price at 1000 USDT\n    trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n    return mode, trader\n\n\nasync def _init_mode(tools, config):\n    mode, trader = tools\n    await mode.initialize(trading_config=config)\n    return mode, mode.producers[0], mode.get_trading_mode_consumers()[0], trader\n\n\nasync def _fill_order(order, trader, trigger_update_callback=True, ignore_open_orders=False, consumer=None,\n                      closed_orders_count=1):\n    initial_len = len(trading_api.get_open_orders(trader.exchange_manager))\n    await order.on_fill(force_fill=True)\n    if order.status == trading_enums.OrderStatus.FILLED:\n        if not ignore_open_orders:\n            assert len(trading_api.get_open_orders(trader.exchange_manager)) == initial_len - closed_orders_count\n        if trigger_update_callback:\n            await asyncio_tools.wait_asyncio_next_cycle()\n        else:\n            with mock.patch.object(consumer, \"create_new_orders\", new=mock.AsyncMock()):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/__init__.py",
    "content": "from .dip_analyser_trading import DipAnalyserTradingMode"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/config/DipAnalyserTradingMode.json",
    "content": "{\n    \"required_strategies\": [\n        \"DipAnalyserStrategyEvaluator\"\n    ],\n    \"sell_orders_count\": 3,\n    \"stop_loss_multiplier\": 0,\n    \"light_weight_price_multiplier\": 1.04,\n    \"medium_weight_price_multiplier\": 1.07,\n    \"heavy_weight_price_multiplier\": 1.1,\n    \"light_weight_volume_multiplier\": 0.5,\n    \"medium_weight_volume_multiplier\": 0.7,\n    \"heavy_weight_volume_multiplier\": 1,\n    \"ignore_exchange_fees\": false,\n    \"emit_trading_signals\": false,\n    \"trading_strategy\": \"\"\n}"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/dip_analyser_trading.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\n\nimport async_channel.constants as channel_constants\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.evaluators_util as evaluators_util\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_evaluators.api as evaluators_api\nimport octobot_evaluators.matrix as matrix\nimport octobot_evaluators.enums as evaluators_enums\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Evaluator.Strategies as Strategies\n\n\nclass DipAnalyserTradingMode(trading_modes.AbstractTradingMode):\n\n    def __init__(self, config, exchange_manager):\n        super().__init__(config, exchange_manager)\n        self.sell_orders_per_buy = 3\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n\n        trading_modes.user_select_order_amount(self, inputs, include_sell=False)\n\n        self.sell_orders_per_buy = self.UI.user_input(\n            \"sell_orders_count\", commons_enums.UserInputTypes.INT, 3, inputs, min_val=1,\n            title=\"Number of sell orders to create after each buy.\"\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeProducer.IGNORE_EXCHANGE_FEES, commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"Ignore exchange fees when creating sell orders. When enabled, 100% of the bought assets will be \"\n                  \"sold, otherwise a small part will be kept to cover exchange fees.\"\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.USE_BUY_MARKET_ORDERS, commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"Use market orders instead of limit orders upon buy signals. Using a market order makes will \"\n                  \"guaranty that each buy signal will create an entry. \"\n                  \"Limit orders (which are priced at 99.5% of the current price) \"\n                  \"can delay an entry for some time to replace an open buy order with a more suitable \"\n                  \"one when the market is very volatile. \"\n                  \"However limit orders might also never be filled and ending up missing a buy opportunity.\"\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.STOP_LOSS_MULTIPLIER, commons_enums.UserInputTypes.FLOAT, 0, inputs,\n            min_val=0, max_val=1,\n            title=\"Stop loss price multiplier: ratio to compute the stop loss price. \"\n                  \"Example: a 0.7 multiplier on a 2000 USDT buy would create a \"\n                  \"stop price at 2000*0.7 = 1400 USDT. Leave at 0 to disable stop losses.\"\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.LIGHT_VOLUME_WEIGHT, commons_enums.UserInputTypes.FLOAT, 0.4, inputs,\n            min_val=0, max_val=1,\n            title=\"Volume multiplier for a buy order on a light volume weight signal.\",\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.MEDIUM_VOLUME_WEIGHT, commons_enums.UserInputTypes.FLOAT, 0.7, inputs,\n            min_val=0, max_val=1,\n            title=\"Volume multiplier for a buy order on a medium volume weight signal.\",\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.HEAVY_VOLUME_WEIGHT, commons_enums.UserInputTypes.FLOAT, 1, inputs,\n            min_val=0, max_val=1,\n            title=\"Volume multiplier for a buy order on a heavy volume weight signal.\",\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.LIGHT_PRICE_WEIGHT, commons_enums.UserInputTypes.FLOAT, 1.04, inputs,\n            min_val=1,\n            title=\"Price multiplier for the top sell order in a light price weight signal.\",\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.MEDIUM_PRICE_WEIGHT, commons_enums.UserInputTypes.FLOAT, 1.07, inputs,\n            min_val=1,\n            title=\"Price multiplier for the top sell order in a medium price weight signal.\",\n        )\n        self.UI.user_input(\n            DipAnalyserTradingModeConsumer.HEAVY_PRICE_WEIGHT, commons_enums.UserInputTypes.FLOAT, 1.1, inputs,\n            min_val=1,\n            title=\"Price multiplier for the top sell order in a heavy price weight signal.\",\n        )\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_current_state(self) -> (str, float):\n        return super().get_current_state()[0] if self.producers[0].state is None else self.producers[0].state.name, \\\n               \"N/A\"\n\n    def get_mode_producer_classes(self) -> list:\n        return [DipAnalyserTradingModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [DipAnalyserTradingModeConsumer]\n\n    async def create_consumers(self) -> list:\n        consumers = await super().create_consumers()\n\n        # order consumer: filter by symbol not be triggered only on this symbol's orders\n        order_consumer = await exchanges_channel.get_chan(trading_personal_data.OrdersChannel.get_name(),\n                                                          self.exchange_manager.id).new_consumer(\n            self._order_notification_callback,\n            symbol=self.symbol if self.symbol else channel_constants.CHANNEL_WILDCARD\n        )\n        return consumers + [order_consumer]\n\n    async def _order_notification_callback(self, exchange, exchange_id, cryptocurrency,\n                                           symbol, order, update_type, is_from_bot):\n        if order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] \\\n                == trading_enums.OrderStatus.FILLED.value and is_from_bot:\n            await self.producers[0].order_filled_callback(order)\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n\nclass DipAnalyserTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    USE_BUY_MARKET_ORDERS = \"use_buy_market_orders\"\n    STOP_LOSS_MULTIPLIER = \"stop_loss_multiplier\"\n    STOP_LOSS_PRICE_MULTIPLIER = decimal.Decimal(0)\n    USE_BUY_MARKET_ORDERS_VALUE = False\n    LIMIT_PRICE_MULTIPLIER = decimal.Decimal(\"0.995\")\n    SOFT_MAX_CURRENCY_RATIO = decimal.Decimal(\"0.33\")\n    # consider a high ratio not to take too much risk and not to prevent order creation either\n    DEFAULT_HOLDING_RATIO = decimal.Decimal(\"0.35\")\n    DEFAULT_FULL_VOLUME = decimal.Decimal(\"0.5\")\n    DEFAULT_SELL_TARGET = decimal.Decimal(\"1\")\n\n    RISK_VOLUME_MULTIPLIER = decimal.Decimal(\"0.2\")\n\n    DELTA_RATIO = decimal.Decimal(\"0.8\")\n\n    ORDER_ID_KEY = \"order_id\"\n    VOLUME_KEY = \"volume\"\n    BUY_PRICE_KEY = \"buy_price\"\n    VOLUME_WEIGHT_KEY = \"volume_weight\"\n    PRICE_WEIGHT_KEY = \"price_weight\"\n\n    LIGHT_VOLUME_WEIGHT = \"light_weight_volume_multiplier\"\n    MEDIUM_VOLUME_WEIGHT = \"medium_weight_volume_multiplier\"\n    HEAVY_VOLUME_WEIGHT = \"heavy_weight_volume_multiplier\"\n    VOLUME_WEIGH_TO_VOLUME_PERCENT = {}\n\n    LIGHT_PRICE_WEIGHT = \"light_weight_price_multiplier\"\n    MEDIUM_PRICE_WEIGHT = \"medium_weight_price_multiplier\"\n    HEAVY_PRICE_WEIGHT = \"heavy_weight_price_multiplier\"\n    PRICE_WEIGH_TO_PRICE_PERCENT = {}\n\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self.sell_targets_by_order_id = {}\n\n    def on_reload_config(self):\n        \"\"\"\n        Called at constructor and after the associated trading mode's reload_config.\n        Implement if necessary\n        \"\"\"\n        self.STOP_LOSS_PRICE_MULTIPLIER = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config.get(self.STOP_LOSS_MULTIPLIER, 0)}\")\n        self.USE_BUY_MARKET_ORDERS_VALUE = self.trading_mode.trading_config.get(self.USE_BUY_MARKET_ORDERS, False)\n        self.PRICE_WEIGH_TO_PRICE_PERCENT = {}\n        self.PRICE_WEIGH_TO_PRICE_PERCENT[1] = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config[self.LIGHT_PRICE_WEIGHT]}\")\n        self.PRICE_WEIGH_TO_PRICE_PERCENT[2] = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config[self.MEDIUM_PRICE_WEIGHT]}\")\n        self.PRICE_WEIGH_TO_PRICE_PERCENT[3] = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config[self.HEAVY_PRICE_WEIGHT]}\")\n\n        self.VOLUME_WEIGH_TO_VOLUME_PERCENT[1] = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config[self.LIGHT_VOLUME_WEIGHT]}\")\n        self.VOLUME_WEIGH_TO_VOLUME_PERCENT[2] = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config[self.MEDIUM_VOLUME_WEIGHT]}\")\n        self.VOLUME_WEIGH_TO_VOLUME_PERCENT[3] = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config[self.HEAVY_VOLUME_WEIGHT]}\")\n\n    async def create_new_orders(self, symbol, final_note, state, **kwargs):\n        timeout = kwargs.get(\"timeout\", trading_constants.ORDER_DATA_FETCHING_TIMEOUT)\n        data = kwargs.get(\"data\", {})\n        if state == trading_enums.EvaluatorStates.LONG.value:\n            volume_weight = data.get(self.VOLUME_WEIGHT_KEY, 1)\n            price_weight = data.get(self.PRICE_WEIGHT_KEY, 1)\n            return await self.create_buy_order(symbol, timeout, volume_weight, price_weight)\n        elif state == trading_enums.EvaluatorStates.SHORT.value:\n            quantity = data.get(self.VOLUME_KEY, decimal.Decimal(\"1\"))\n            buy_order_id = data[self.ORDER_ID_KEY]\n            sell_weight = self._get_sell_target_for_registered_order(buy_order_id)\n            sell_base = data[self.BUY_PRICE_KEY]\n            return await self.create_sell_orders(symbol, timeout, self.trading_mode.sell_orders_per_buy,\n                                                 quantity, sell_weight, sell_base, buy_order_id)\n        self.logger.error(f\"Unknown required order action: data= {data}\")\n\n    async def create_buy_order(self, symbol, timeout, volume_weight, price_weight):\n        current_order = None\n        try:\n            current_symbol_holding, current_market_holding, market_quantity, price, symbol_market = \\\n                await trading_personal_data.get_pre_order_data(self.exchange_manager, symbol=symbol, timeout=timeout)\n            max_buy_size = market_quantity\n            if self.exchange_manager.is_future:\n                max_buy_size, is_increasing_position = trading_personal_data.get_futures_max_order_size(\n                    self.exchange_manager, symbol, trading_enums.TradeOrderSide.BUY,\n                    price, False, current_symbol_holding, market_quantity\n                )\n\n            base = symbol_util.parse_symbol(symbol).base\n            created_orders = []\n            orders_should_have_been_created = False\n            ctx = script_keywords.get_base_context(self.trading_mode, symbol)\n            order_type = trading_enums.TraderOrderType.BUY_MARKET \\\n                if self.USE_BUY_MARKET_ORDERS_VALUE else trading_enums.TraderOrderType.BUY_LIMIT\n            quantity = await self._get_buy_quantity_from_weight(ctx, volume_weight, max_buy_size, base)\n            limit_price = trading_personal_data.decimal_adapt_price(\n                symbol_market,\n                price if self.USE_BUY_MARKET_ORDERS_VALUE else self.get_limit_price(price)\n            )\n            quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                self.exchange_manager, symbol, order_type, quantity, limit_price, trading_enums.TradeOrderSide.BUY\n            )\n            for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    quantity,\n                    limit_price,\n                    symbol_market):\n                orders_should_have_been_created = True\n                current_order = trading_personal_data.create_order_instance(\n                    trader=self.exchange_manager.trader,\n                    order_type=order_type,\n                    symbol=symbol,\n                    current_price=price,\n                    quantity=order_quantity,\n                    price=order_price,\n                )\n                if created_order := await self.trading_mode.create_order(current_order):\n                    created_orders.append(created_order)\n                    self._register_buy_order(created_order.order_id, price_weight)\n            if created_orders:\n                return created_orders\n            if orders_should_have_been_created:\n                raise trading_errors.OrderCreationError()\n            raise trading_errors.MissingMinimalExchangeTradeVolume()\n\n        except (trading_errors.MissingFunds,\n                trading_errors.MissingMinimalExchangeTradeVolume,\n                trading_errors.OrderCreationError,\n                trading_errors.InvalidCancelPolicyError):\n            raise\n        except Exception as e:\n            self.logger.exception(\n                e, True, f\"Failed to create order : {e}. Order: {current_order if current_order else None}\"\n            )\n            return []\n\n    async def create_sell_orders(\n        self, symbol, timeout, sell_orders_count, quantity, sell_weight, sell_base, buy_order_id\n    ):\n        current_order = None\n        try:\n            reduce_only = False\n            if self.exchange_manager.is_future and await self.wait_for_active_position(symbol, timeout):\n                # can use reduce only orders now that the position is active\n                reduce_only = True\n            current_symbol_holding, current_market_holding, market_quantity, price, symbol_market = \\\n                await trading_personal_data.get_pre_order_data(self.exchange_manager, symbol=symbol, timeout=timeout)\n            max_sell_size = current_symbol_holding\n            if self.exchange_manager.is_future:\n                max_sell_size, is_increasing_position = trading_personal_data.get_futures_max_order_size(\n                    self.exchange_manager, symbol, trading_enums.TradeOrderSide.SELL,\n                    price, False, current_symbol_holding, market_quantity\n                )\n            created_orders = []\n            orders_should_have_been_created = False\n            sell_max_quantity = decimal.Decimal(min(decimal.Decimal(f\"{max_sell_size}\"), quantity))\n            to_create_orders = self._generate_sell_orders(sell_orders_count, sell_max_quantity, sell_weight,\n                                                          sell_base, symbol_market)\n            for order_quantity, order_price in to_create_orders:\n                orders_should_have_been_created = True\n                current_limit_order = trading_personal_data.create_order_instance(\n                    trader=self.exchange_manager.trader,\n                    order_type=trading_enums.TraderOrderType.SELL_LIMIT,\n                    symbol=symbol,\n                    current_price=sell_base,\n                    quantity=order_quantity,\n                    price=order_price,\n                    reduce_only=reduce_only,\n                    associated_entry_id=buy_order_id,\n                )\n                created_sell_order, created_stop_order = await self._create_exit_with_stop_loss_if_enabled(\n                    current_limit_order, sell_base, symbol_market, buy_order_id\n                )\n                created_orders.append(created_sell_order)\n                if created_stop_order:\n                    created_orders.append(created_stop_order)\n            if created_orders:\n                return created_orders\n            if orders_should_have_been_created:\n                raise trading_errors.OrderCreationError()\n            raise trading_errors.MissingMinimalExchangeTradeVolume()\n\n        except (trading_errors.MissingFunds,\n                trading_errors.MissingMinimalExchangeTradeVolume,\n                trading_errors.OrderCreationError):\n            raise\n        except Exception as e:\n            self.logger.exception(\n                e, True, f\"Failed to create order : {e} ({e.__class__.__name__}). Order: \"\n                f\"{current_order if current_order else None}\"\n            )\n            return []\n\n    async def _create_exit_with_stop_loss_if_enabled(self, sell_order_to_create, sell_base, symbol_market, buy_order_id):\n        current_stop_order = None\n        if self.STOP_LOSS_PRICE_MULTIPLIER and sell_order_to_create:\n            stop_price = sell_base * self.STOP_LOSS_PRICE_MULTIPLIER\n            oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(\n                trading_personal_data.OneCancelsTheOtherOrderGroup,\n                active_order_swap_strategy=trading_personal_data.StopFirstActiveOrderSwapStrategy()\n            )\n            sell_order_to_create.add_to_order_group(oco_group)\n            current_stop_order = trading_personal_data.create_order_instance(\n                trader=self.exchange_manager.trader,\n                order_type=trading_enums.TraderOrderType.STOP_LOSS,\n                symbol=sell_order_to_create.symbol,\n                current_price=trading_personal_data.adapt_price(symbol_market, stop_price),\n                quantity=sell_order_to_create.origin_quantity,\n                price=stop_price,\n                side=trading_enums.TradeOrderSide.SELL,\n                reduce_only=True,\n                group=oco_group,\n                associated_entry_id=buy_order_id,\n            )\n            # in futures, inactive orders are not necessary\n            if self.exchange_manager.trader.enable_inactive_orders and not self.exchange_manager.is_future:\n                await oco_group.active_order_swap_strategy.apply_inactive_orders([sell_order_to_create, current_stop_order])\n        created_sell_order = await self.trading_mode.create_order(sell_order_to_create)\n        created_stop_order = None\n        if created_sell_order and created_sell_order.is_open() and current_stop_order:\n            created_stop_order = await self.trading_mode.create_order(current_stop_order)\n            self.logger.debug(f\"Grouping orders: {sell_order_to_create} and {created_stop_order}\")\n        return created_sell_order, created_stop_order\n\n    def _register_buy_order(self, order_id, price_weight):\n        self.sell_targets_by_order_id[order_id] = price_weight\n\n    def unregister_buy_order(self, order_id):\n        self.sell_targets_by_order_id.pop(order_id, None)\n\n    async def _get_buy_quantity_from_weight(self, ctx, volume_weight, market_quantity, currency):\n        weighted_volume = self.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight]\n        # high risk is making larger orders, low risk is making smaller ones\n        risk_multiplier = 1 + ((self.exchange_manager.trader.risk - decimal.Decimal(\"0.5\")) * self.RISK_VOLUME_MULTIPLIER)\n        weighted_volume = min(weighted_volume * risk_multiplier, trading_constants.ONE)\n        # check configured quantity\n        if user_amount := trading_modes.get_user_selected_order_amount(self.trading_mode,\n                                                                       trading_enums.TradeOrderSide.BUY):\n            return await script_keywords.get_amount_from_input_amount(\n                context=ctx,\n                input_amount=user_amount,\n                side=trading_enums.TradeOrderSide.BUY.value,\n                reduce_only=False,\n                is_stop_order=False,\n                use_total_holding=False,\n            ) * weighted_volume\n        traded_assets_count = self.get_number_of_traded_assets()\n        if traded_assets_count == 1:\n            return market_quantity * self.DEFAULT_FULL_VOLUME * weighted_volume\n        elif traded_assets_count == 2:\n            return market_quantity * self.SOFT_MAX_CURRENCY_RATIO * weighted_volume\n        else:\n            currency_ratio = trading_constants.ZERO\n            if currency != self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market:\n                # if currency (base) is not ref market => need to check holdings ratio not to spend all ref market\n                # into one currency (at least 3 traded assets are available here)\n                try:\n                    currency_ratio = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                        portfolio_value_holder.get_holdings_ratio(currency)\n                except trading_errors.MissingPriceDataError:\n                    # Can happen when ref market is not in the pair, data will be available later (ticker is now\n                    # registered)\n                    currency_ratio = self.DEFAULT_HOLDING_RATIO\n            # linear function of % holding in this currency: volume_ratio is in [0, SOFT_MAX_CURRENCY_RATIO*0.8]\n            volume_ratio = self.SOFT_MAX_CURRENCY_RATIO * \\\n                (1 - min(currency_ratio * self.DELTA_RATIO, trading_constants.ONE))\n            return market_quantity * volume_ratio * weighted_volume\n\n    def _get_sell_target_for_registered_order(self, order_id):\n        try:\n            return self.sell_targets_by_order_id[order_id]\n        except KeyError:\n            if not self.sell_targets_by_order_id:\n                self.logger.warning(f\"No registered buy orders, therefore no sell target for order with id \"\n                                    f\"{order_id}. Using default sell target: {self.DEFAULT_SELL_TARGET}.\")\n            else:\n                self.logger.warning(f\"No sell target for order with id {order_id}. \"\n                                    f\"Using default sell target: {self.DEFAULT_SELL_TARGET}.\")\n            return self.DEFAULT_SELL_TARGET\n\n    def get_limit_price(self, price):\n        # buy very close from current price\n        return price * self.LIMIT_PRICE_MULTIPLIER\n\n    def _generate_sell_orders(self, sell_orders_count, quantity, sell_weight, sell_base, symbol_market):\n        volume_with_price = []\n        sell_max = sell_base * self.PRICE_WEIGH_TO_PRICE_PERCENT[sell_weight]\n        adapted_sell_orders_count, increment = trading_personal_data.get_split_orders_count_and_increment(\n            sell_base, sell_max, quantity, sell_orders_count, symbol_market, True\n        )\n        if adapted_sell_orders_count:\n            order_volume = quantity / adapted_sell_orders_count\n            total_volume = 0\n            for i in range(adapted_sell_orders_count):\n                order_price = sell_base + (increment * (i + 1))\n                for adapted_quantity, adapted_price \\\n                        in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                        order_volume,\n                        order_price,\n                        symbol_market):\n                    total_volume += adapted_quantity\n                    volume_with_price.append((adapted_quantity, adapted_price))\n            if not volume_with_price:\n                volume_with_price.append((quantity, trading_personal_data.decimal_adapt_price(symbol_market,\n                                                                                              sell_base + increment)))\n                total_volume += quantity\n            if total_volume < quantity:\n                # ensure the whole target quantity is used\n                full_quantity = volume_with_price[-1][0] + quantity - total_volume\n                volume_with_price[-1] = (full_quantity, volume_with_price[-1][1])\n        return volume_with_price\n\n\nclass DipAnalyserTradingModeProducer(trading_modes.AbstractTradingModeProducer):\n    IGNORE_EXCHANGE_FEES = \"ignore_exchange_fees\"\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        self.ignore_exchange_fees = False\n        super().__init__(channel, config, trading_mode, exchange_manager)\n\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n        self.first_trigger = True\n\n        self.last_buy_candle = None\n        self.base = symbol_util.parse_symbol(self.trading_mode.symbol).base\n\n    def on_reload_config(self):\n        \"\"\"\n        Called at constructor and after the associated trading mode's reload_config.\n        Implement if necessary\n        \"\"\"\n        self.ignore_exchange_fees = self.trading_mode.trading_config.get(self.IGNORE_EXCHANGE_FEES, False)\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        await super().stop()\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        # Strategies analysis\n        for evaluated_strategy_node in matrix.get_tentacles_value_nodes(\n                matrix_id,\n                matrix.get_tentacle_nodes(matrix_id,\n                                          exchange_name=self.exchange_name,\n                                          tentacle_type=evaluators_enums.EvaluatorMatrixTypes.STRATEGIES.value,\n                                          tentacle_name=Strategies.DipAnalyserStrategyEvaluator.get_name()),\n                symbol=symbol):\n            if evaluators_util.check_valid_eval_note(evaluators_api.get_value(evaluated_strategy_node),\n                                                     evaluators_api.get_type(evaluated_strategy_node),\n                                                     Strategies.DipAnalyserStrategyEvaluator.get_eval_type()):\n                self.final_eval = evaluators_api.get_value(evaluated_strategy_node)\n                await self.create_state()\n\n    async def create_state(self):\n        self.state = trading_enums.EvaluatorStates.LONG\n        if self.first_trigger:\n            # can't rely on previous execution buy orders: need plans for sell orders\n            await self._cancel_buy_orders()\n            self.first_trigger = False\n        if self.final_eval != commons_constants.START_PENDING_EVAL_NOTE:\n            volume_weight = self.final_eval[\"volume_weight\"]\n            price_weight = self.final_eval[\"price_weight\"]\n            await self._create_bottom_order(self.final_eval[\"current_candle_time\"], volume_weight, price_weight)\n\n    async def order_filled_callback(self, filled_order):\n        if filled_order[trading_enums.ExchangeConstantsOrderColumns.SIDE.value] \\\n                == trading_enums.TradeOrderSide.BUY.value:\n            self.state = trading_enums.EvaluatorStates.SHORT\n            paid_fees = 0 if self.ignore_exchange_fees else \\\n                decimal.Decimal(f\"{trading_personal_data.total_fees_from_order_dict(filled_order, self.base)}\")\n            sell_quantity = \\\n                decimal.Decimal(f\"{filled_order[trading_enums.ExchangeConstantsOrderColumns.FILLED.value]}\") - paid_fees\n            price = decimal.Decimal(f\"{filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]}\")\n            await self._create_sell_order_if_enabled(\n                filled_order[trading_enums.ExchangeConstantsOrderColumns.ID.value],\n                sell_quantity,\n                price\n            )\n\n    async def _create_sell_order_if_enabled(self, order_id, sell_quantity, buy_price):\n        if self.exchange_manager.trader.is_enabled:\n            data = {\n                DipAnalyserTradingModeConsumer.ORDER_ID_KEY: order_id,\n                DipAnalyserTradingModeConsumer.VOLUME_KEY: sell_quantity,\n                DipAnalyserTradingModeConsumer.BUY_PRICE_KEY: buy_price,\n            }\n            await self.submit_trading_evaluation(cryptocurrency=self.trading_mode.cryptocurrency,\n                                                 symbol=self.trading_mode.symbol,\n                                                 time_frame=None,\n                                                 state=trading_enums.EvaluatorStates.SHORT,\n                                                 data=data)\n\n    async def _create_bottom_order(self, notification_candle_time, volume_weight, price_weight):\n        self.logger.info(f\"** New buy signal for ** : {self.trading_mode.symbol}\")\n        # call orders creation method\n        await self._create_buy_order_if_enabled(notification_candle_time, volume_weight, price_weight)\n\n    async def _create_buy_order_if_enabled(self, notification_candle_time, volume_weight, price_weight):\n        if self.exchange_manager.trader.is_enabled:\n            # cancel previous by orders if any\n            cancelled_orders = await self._cancel_buy_orders()\n            if self.last_buy_candle == notification_candle_time and cancelled_orders or \\\n               self.last_buy_candle != notification_candle_time:\n                # if subsequent notification from the same candle: only create order if able to cancel the previous buy\n                # to avoid multiple order on the same candle\n                data = {\n                    DipAnalyserTradingModeConsumer.VOLUME_WEIGHT_KEY: volume_weight,\n                    DipAnalyserTradingModeConsumer.PRICE_WEIGHT_KEY: price_weight,\n                }\n                await self.submit_trading_evaluation(cryptocurrency=self.trading_mode.cryptocurrency,\n                                                     symbol=self.trading_mode.symbol,\n                                                     time_frame=None,\n                                                     state=trading_enums.EvaluatorStates.LONG,\n                                                     data=data)\n                self.last_buy_candle = notification_candle_time\n            else:\n                self.logger.debug(f\"Trader ignored buy signal for {self.trading_mode.symbol}: \"\n                                  f\"buy order already filled.\")\n\n    @classmethod\n    def get_should_cancel_loaded_orders(cls):\n        return True\n\n    def _get_current_buy_orders(self):\n        return [order\n                for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(\n                    self.trading_mode.symbol)\n                if order.side == trading_enums.TradeOrderSide.BUY]\n\n    async def _cancel_buy_orders(self):\n        cancelled_orders = False\n        if self.exchange_manager.trader.is_enabled:\n            for order in self._get_current_buy_orders():\n                try:\n                    cancelled_orders = await self.trading_mode.cancel_order(order) or cancelled_orders\n                except (trading_errors.OrderCancelError, trading_errors.UnexpectedExchangeSideOrderStateError) as err:\n                    self.logger.warning(f\"Skipping order cancel: {err}\")\n                    # order can't be cancelled: don't set cancelled_orders to True\n        return cancelled_orders\n"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"DipAnalyserTradingMode\"],\n  \"tentacles-requirements\": [\"dip_analyser_strategy_evaluator\"]\n}"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/resources/DipAnalyserTradingMode.md",
    "content": "DipAnalyserTradingMode is a trading mode adapted to **volatile markets**.\n\nIt will look for local market bottoms, weight them and buy these bottoms. It never sells except after a buy order is\nfilled.\n\nWhen a **buy order is filled, sell orders will automatically be created at a higher price**\nthan this of the filled buy order. The number of sell orders created after each buy can be configured.\n\nA higher risk configuration will make larger buy orders when order size is not configured.\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/dip-analyser-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=DipAnalyserTradingModeDocs\">\nfull Dip analyser trading mode guide</a>.\n\n### Good to know\n\n- Ensure **enough funds are available in your portfolio** for OctoBot to place the **initial buy orders**.\n- Sell orders are never cancelled by this strategy unless stop losses are enabled,  therefore it is not advised to use it on\ncontinued downtrends without using stop losses: funds might get locked in open sell orders.\n- Limit buy orders might be automatically cancelled and replaced when a better buy opportunity is identified.\n\n_This trading mode supports PNL history._\n"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/dip_analyser_trading_mode/tests/test_dip_analyser_trading_mode.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport pytest_asyncio\nimport os.path\nimport asyncio\nimport mock\nimport decimal\n\nimport async_channel.util as channel_util\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.enums as commons_enum\nimport octobot_commons.tests.test_config as test_config\nimport octobot_backtesting.api as backtesting_api\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.modes.script_keywords as script_keywords\nimport octobot_commons.constants as commons_constants\nimport tentacles.Evaluator.TA as TA\nimport tentacles.Evaluator.Strategies as Strategies\nimport tentacles.Trading.Mode as Mode\nimport tests.test_utils.memory_check_util as memory_check_util\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def tools():\n    trader = None\n    try:\n        tentacles_manager_api.reload_tentacle_info()\n        producer, consumer, trader = await _get_tools()\n        yield producer, consumer, trader\n    finally:\n        if trader:\n            await _stop(trader.exchange_manager)\n\n\nasync def test_run_independent_backtestings_with_memory_check():\n    \"\"\"\n    Should always be called first here to avoid other tests' related memory check issues\n    \"\"\"\n    tentacles_setup_config = tentacles_manager_api.create_tentacles_setup_config_with_tentacles(\n        Mode.DipAnalyserTradingMode,\n        Strategies.DipAnalyserStrategyEvaluator,\n        TA.KlingerOscillatorReversalConfirmationMomentumEvaluator,\n        TA.RSIWeightMomentumEvaluator\n    )\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_TIME_FRAME] = [commons_enum.TimeFrames.FOUR_HOURS]\n    await memory_check_util.run_independent_backtestings_with_memory_check(config, tentacles_setup_config)\n\n\nasync def test_init(tools):\n    producer, consumer, trader = tools\n    # trading mode\n    assert producer.trading_mode is consumer.trading_mode\n    assert producer.trading_mode.sell_orders_per_buy == 3\n\n    # producer\n    assert producer.last_buy_candle is None\n    assert producer.first_trigger\n\n    # consumer\n    assert consumer.sell_targets_by_order_id == {}\n    assert consumer.PRICE_WEIGH_TO_PRICE_PERCENT == {\n        1: decimal.Decimal(\"1.04\"),\n        2: decimal.Decimal(\"1.07\"),\n        3: decimal.Decimal(\"1.1\"),\n    }\n    assert consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT == {\n        1: decimal.Decimal(\"0.5\"),\n        2: decimal.Decimal(\"0.7\"),\n        3: decimal.Decimal(\"1\"),\n    }\n\n\nasync def test_create_limit_bottom_order(tools):\n    producer, consumer, trader = tools\n\n    price = decimal.Decimal(\"1000\")\n    market_quantity = decimal.Decimal(\"2\")\n    volume_weight = decimal.Decimal(\"1\")\n    risk_multiplier = decimal.Decimal(\"1.1\")\n\n    market_status = producer.exchange_manager.exchange.get_market_status(producer.trading_mode.symbol, with_fixer=False)\n    _origin_decimal_adapt_order_quantity_because_fees = trading_personal_data.decimal_adapt_order_quantity_because_fees\n\n    def _decimal_adapt_order_quantity_because_fees(\n        exchange_manager, symbol: str, order_type: trading_enums.TraderOrderType, quantity: decimal.Decimal,\n        price: decimal.Decimal, side: trading_enums.TradeOrderSide,\n    ):\n        return quantity\n\n    with mock.patch.object(\n            trading_personal_data, \"decimal_adapt_order_quantity_because_fees\",\n            mock.Mock(side_effect=_decimal_adapt_order_quantity_because_fees)\n    ) as decimal_adapt_order_quantity_because_fees_mock:\n        await producer._create_bottom_order(1, volume_weight, 1)\n        # create as task to allow creator's queue to get processed\n        await asyncio.create_task(_check_open_orders_count(trader, 1))\n        await asyncio_tools.wait_asyncio_next_cycle()\n\n        order = trading_api.get_open_orders(trader.exchange_manager)[0]\n        adapted_args = list(decimal_adapt_order_quantity_because_fees_mock.mock_calls[0].args)\n        adapted_args[3] = trading_personal_data.decimal_adapt_quantity(market_status, adapted_args[3])\n        adapted_args[4] = trading_personal_data.decimal_adapt_price(market_status, adapted_args[4])\n        assert adapted_args == [\n            producer.exchange_manager, producer.trading_mode.symbol, trading_enums.TraderOrderType.BUY_LIMIT,\n            order.origin_quantity,\n            order.origin_price,\n            trading_enums.TradeOrderSide.BUY,\n        ]\n\n        assert isinstance(order, trading_personal_data.BuyLimitOrder)\n        expected_quantity = market_quantity * risk_multiplier * \\\n            consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * \\\n            consumer.SOFT_MAX_CURRENCY_RATIO\n        assert order.origin_quantity == expected_quantity\n\n        expected_price = price * consumer.LIMIT_PRICE_MULTIPLIER\n        assert order.origin_price == expected_price\n        portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n        assert portfolio.get_currency_portfolio(\"USDT\").available > trading_constants.ZERO\n\n        assert order.order_id in consumer.sell_targets_by_order_id\n\n\nasync def test_create_market_bottom_order(tools):\n    producer, consumer, trader = tools\n\n    price = decimal.Decimal(\"1000\")\n    market_quantity = decimal.Decimal(\"2\")\n    volume_weight = decimal.Decimal(\"1\")\n    risk_multiplier = decimal.Decimal(\"1.1\")\n    consumer.USE_BUY_MARKET_ORDERS_VALUE = True\n    trades = trading_api.get_trade_history(trader.exchange_manager)\n    assert trades == []\n\n    market_status = producer.exchange_manager.exchange.get_market_status(producer.trading_mode.symbol, with_fixer=False)\n    _origin_decimal_adapt_order_quantity_because_fees = trading_personal_data.decimal_adapt_order_quantity_because_fees\n\n    def _decimal_adapt_order_quantity_because_fees(\n        exchange_manager, symbol: str, order_type: trading_enums.TraderOrderType, quantity: decimal.Decimal,\n        price: decimal.Decimal, side: trading_enums.TradeOrderSide,\n    ):\n        return quantity\n\n    with mock.patch.object(\n            trading_personal_data, \"decimal_adapt_order_quantity_because_fees\",\n            mock.Mock(side_effect=_decimal_adapt_order_quantity_because_fees)\n    ) as decimal_adapt_order_quantity_because_fees_mock:\n        await producer._create_bottom_order(1, volume_weight, 1)\n        # create as task to allow creator's queue to get processed (market order is instantly filled)\n        await asyncio.create_task(_check_open_orders_count(trader, 0))\n        await asyncio_tools.wait_asyncio_next_cycle()\n\n        trade = trading_api.get_trade_history(trader.exchange_manager)[0]\n        adapted_args = list(decimal_adapt_order_quantity_because_fees_mock.mock_calls[0].args)\n        adapted_args[3] = trading_personal_data.decimal_adapt_quantity(market_status, adapted_args[3])\n        adapted_args[4] = trading_personal_data.decimal_adapt_price(market_status, adapted_args[4])\n        assert adapted_args == [\n            producer.exchange_manager, producer.trading_mode.symbol, trading_enums.TraderOrderType.BUY_MARKET,\n            trade.origin_quantity,\n            trade.origin_price,\n            trading_enums.TradeOrderSide.BUY,\n        ]\n\n        assert trade.trade_type == trading_enums.TraderOrderType.BUY_MARKET\n        expected_quantity = market_quantity * risk_multiplier * \\\n            consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * \\\n            consumer.SOFT_MAX_CURRENCY_RATIO\n        assert trade.origin_quantity == expected_quantity\n\n        # no price multiplier used as it is a market order (use market price)\n        assert trade.origin_price == price\n        portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n        assert portfolio.get_currency_portfolio(\"USDT\").available > trading_constants.ZERO\n\n        assert trade.origin_order_id in consumer.sell_targets_by_order_id\n\n\nasync def test_create_bottom_order_with_configured_quantity(tools):\n    producer, consumer, trader = tools\n\n    producer.trading_mode.trading_config[trading_constants.CONFIG_BUY_ORDER_AMOUNT] = \\\n        f\"20{script_keywords.QuantityType.PERCENT.value}\"\n    price = decimal.Decimal(\"1000\")\n    market_quantity = decimal.Decimal(\"2\")\n    volume_weight = decimal.Decimal(\"1\")\n    risk_multiplier = decimal.Decimal(\"1.1\")\n    # force portfolio value\n    trader.exchange_manager.exchange_personal_data. \\\n        portfolio_manager.portfolio_value_holder.portfolio_current_value = decimal.Decimal(1)\n    await producer._create_bottom_order(1, volume_weight, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n    order = trading_api.get_open_orders(trader.exchange_manager)[0]\n    default_expected_quantity = market_quantity * risk_multiplier * \\\n        consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * \\\n        consumer.SOFT_MAX_CURRENCY_RATIO\n    expected_quantity = market_quantity * risk_multiplier * \\\n        consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * \\\n        decimal.Decimal(\"0.2\")\n    assert default_expected_quantity != expected_quantity\n    assert order.origin_quantity == expected_quantity\n\n    expected_price = price * consumer.LIMIT_PRICE_MULTIPLIER\n    assert order.origin_price == expected_price\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n    assert trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available  > trading_constants.ZERO\n\n    assert order.order_id in consumer.sell_targets_by_order_id\n\n\nasync def test_create_too_large_bottom_order(tools):\n    producer, consumer, trader = tools\n\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available = decimal.Decimal(\"200000000000000\")\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").total = decimal.Decimal(\"200000000000000\")\n    await producer._create_bottom_order(1, 1, 1)\n    # create as task to allow creator's queue to get processed\n    for _ in range(37):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, 37))\n    assert trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available > trading_constants.ZERO\n\n\nasync def test_create_too_small_bottom_order(tools):\n    producer, consumer, trader = tools\n\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available = decimal.Decimal(\"0.01\")\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").total = decimal.Decimal(\"0.01\")\n    await producer._create_bottom_order(1, 1, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 0))\n    assert trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available == decimal.Decimal(\"0.01\")\n\n\nasync def test_create_bottom_order_replace_current(tools):\n    producer, consumer, trader = tools\n\n    price = decimal.Decimal(\"1000\")\n    market_quantity = decimal.Decimal(\"2\")\n    volume_weight = decimal.Decimal(\"1\")\n    risk_multiplier = decimal.Decimal(\"1.1\")\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n\n    # first order\n    await producer._create_bottom_order(1, volume_weight, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n    first_order = trading_api.get_open_orders(trader.exchange_manager)[0]\n    assert first_order.status == trading_enums.OrderStatus.OPEN\n    expected_quantity = market_quantity * risk_multiplier * \\\n        consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * consumer.SOFT_MAX_CURRENCY_RATIO\n    assert first_order.origin_quantity == expected_quantity\n    expected_price = price * consumer.LIMIT_PRICE_MULTIPLIER\n    assert first_order.origin_price == expected_price\n    available_after_order = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available\n    assert available_after_order > trading_constants.ZERO\n    assert first_order.order_id in consumer.sell_targets_by_order_id\n\n    # second order, same weight\n    await producer._create_bottom_order(1, volume_weight, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n    second_order = trading_api.get_open_orders(trader.exchange_manager)[0]\n    assert first_order.status == trading_enums.OrderStatus.CANCELED\n    assert second_order.status == trading_enums.OrderStatus.OPEN\n    assert second_order is not first_order\n    assert second_order.origin_quantity == first_order.origin_quantity\n    assert second_order.origin_price == first_order.origin_price\n    assert trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available == available_after_order\n    # order still in sell_targets_by_order_id: cancelling orders doesn't remove them for this\n    assert first_order.order_id in consumer.sell_targets_by_order_id\n    assert second_order.order_id in consumer.sell_targets_by_order_id\n\n    # third order, different weight\n    volume_weight = 3\n    await producer._create_bottom_order(1, volume_weight, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n    third_order = trading_api.get_open_orders(trader.exchange_manager)[0]\n    assert second_order.status == trading_enums.OrderStatus.CANCELED\n    assert third_order.status == trading_enums.OrderStatus.OPEN\n    assert third_order is not second_order and third_order is not first_order\n    expected_quantity = market_quantity * \\\n        consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * consumer.SOFT_MAX_CURRENCY_RATIO\n    assert third_order.origin_quantity != first_order.origin_quantity\n    assert third_order.origin_quantity == expected_quantity\n    assert third_order.origin_price == first_order.origin_price\n    available_after_third_order = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available\n    assert available_after_third_order < available_after_order\n    assert second_order.order_id in consumer.sell_targets_by_order_id\n    assert third_order.order_id in consumer.sell_targets_by_order_id\n\n    # fill third order\n    await _fill_order(third_order, trader, trigger_update_callback=False, consumer=consumer)\n\n    # fourth order: can't be placed: an order on this candle got filled\n    volume_weight = 3\n    await producer._create_bottom_order(1, volume_weight, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 0))\n\n    # fifth order: in the next candle\n    volume_weight = 2\n    new_market_quantity = decimal.Decimal(f'{trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available}') \\\n        / price\n    await producer._create_bottom_order(2, volume_weight, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n    fifth_order = trading_api.get_open_orders(trader.exchange_manager)[0]\n    assert third_order.status == trading_enums.OrderStatus.FILLED\n    assert fifth_order.status == trading_enums.OrderStatus.OPEN\n    assert fifth_order is not third_order and fifth_order is not second_order and fifth_order is not first_order\n    expected_quantity = new_market_quantity * risk_multiplier * \\\n        consumer.VOLUME_WEIGH_TO_VOLUME_PERCENT[volume_weight] * consumer.SOFT_MAX_CURRENCY_RATIO\n    assert fifth_order.origin_quantity != first_order.origin_quantity\n    assert fifth_order.origin_quantity != third_order.origin_quantity\n    assert fifth_order.origin_quantity == trading_personal_data.decimal_trunc_with_n_decimal_digits(expected_quantity, 8)\n    assert fifth_order.origin_price == first_order.origin_price\n    assert trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USDT\").available < available_after_third_order\n    assert first_order.order_id in consumer.sell_targets_by_order_id\n    assert second_order.order_id in consumer.sell_targets_by_order_id\n\n    # third_order still in _get_order_identifier to keep history\n    assert third_order.order_id in consumer.sell_targets_by_order_id\n    assert fifth_order.order_id in consumer.sell_targets_by_order_id\n\n\nasync def test_create_sell_orders_without_stop_loss(tools):\n    producer, consumer, trader = tools\n\n    sell_quantity = decimal.Decimal(\"5\")\n    sell_target = 2\n    buy_price = decimal.Decimal(\"100\")\n    order_id = \"a\"\n    consumer.STOP_LOSS_PRICE_MULTIPLIER = trading_constants.ZERO\n    consumer.sell_targets_by_order_id[order_id] = sell_target\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    assert all(o.associated_entry_ids == [\"a\"] for o in open_orders)\n    assert all(isinstance(o, trading_personal_data.SellLimitOrder) for o in open_orders)\n    assert not any(isinstance(o, trading_personal_data.StopLossOrder) for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    # rounding because orders to create volumes are X.33333\n    assert sell_quantity * decimal.Decimal(\"0.9999\") <= total_sell_quantity <= sell_quantity\n\n    max_price = buy_price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[sell_target]\n    increment = (max_price - buy_price) / consumer.trading_mode.sell_orders_per_buy\n    assert open_orders[0].origin_price == \\\n           trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + increment, 8)\n    assert open_orders[1].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + 2 * increment, 8)\n    assert open_orders[2].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + 3 * increment, 8)\n\n    # now fill a sell order\n    await _fill_order(open_orders[0], trader, trigger_update_callback=False, consumer=consumer)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy - 1))\n    sell_quantity = decimal.Decimal(\"3\")\n    sell_target = 3\n    buy_price = decimal.Decimal(\"2525\")\n    order_id_2 = \"b\"\n    consumer.sell_targets_by_order_id[order_id_2] = sell_target\n    await producer._create_sell_order_if_enabled(order_id_2, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy * 2 - 1))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    assert all(o.associated_entry_ids == [\"a\"] for o in open_orders[:2])\n    assert all(o.associated_entry_ids == [\"b\"] for o in open_orders[2:])\n    assert all(isinstance(o, trading_personal_data.SellLimitOrder) for o in open_orders)\n    assert not any(isinstance(o, trading_personal_data.StopLossOrder) for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders if o.origin_price > 150)\n    assert total_sell_quantity == sell_quantity\n\n    max_price = buy_price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[sell_target]\n    increment = (max_price - buy_price) / consumer.trading_mode.sell_orders_per_buy\n    assert open_orders[2 + 0].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + increment, 8)\n    assert open_orders[2 + 1].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + 2 * increment, 8)\n    assert open_orders[2 + 2].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + 3 * increment, 8)\n\n    # now fill a sell order\n    await _fill_order(open_orders[-1], trader, trigger_update_callback=False, consumer=consumer)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy * 2 - 2))\n\n\nasync def test_create_sell_orders_with_stop_loss(tools):\n    producer, consumer, trader = tools\n\n    trader.enable_inactive_orders = True\n    sell_quantity = decimal.Decimal(\"5\")\n    sell_target = 2\n    buy_price = decimal.Decimal(\"100\")\n    order_id = \"a\"\n    consumer.STOP_LOSS_PRICE_MULTIPLIER = decimal.Decimal(\"0.75\")\n    stop_price = consumer.STOP_LOSS_PRICE_MULTIPLIER * buy_price\n    consumer.sell_targets_by_order_id[order_id] = sell_target\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    # * 2 to account for the stop order associated to each sell order\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy * 2))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.associated_entry_ids == [\"a\"] for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    assert any(isinstance(o, (trading_personal_data.SellLimitOrder, trading_personal_data.StopLossOrder))\n               for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    # rounding because orders to create volumes are X.33333\n    assert sell_quantity * decimal.Decimal(\"0.9999\") * 2 <= total_sell_quantity <= sell_quantity * 2\n\n    # ensure order quantity and groups\n    limit_orders = [o for o in open_orders if isinstance(o, trading_personal_data.SellLimitOrder)]\n    stop_orders = [o for o in open_orders if isinstance(o, trading_personal_data.StopLossOrder)]\n    assert len(limit_orders) == len(stop_orders)\n    for limit, stop in zip(limit_orders, stop_orders):\n        assert isinstance(limit.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)\n        assert limit.order_group is stop.order_group\n        assert limit.origin_quantity == stop.origin_quantity\n        assert limit.origin_price > stop.origin_price\n        assert stop.origin_price == stop_price\n        assert stop.is_active is True\n        assert limit.is_active is False\n        group_orders = trader.exchange_manager.exchange_personal_data.orders_manager.get_order_from_group(\n            limit.order_group.name\n        )\n        assert group_orders == [limit, stop]\n\n    # now fill a sell order\n    await _fill_order(limit_orders[0], trader, trigger_update_callback=False, consumer=consumer, closed_orders_count=2)\n    # create as task to allow creator's queue to get processed\n    # also check that associated stop loss is cancelled\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy * 2 - 2))\n    sell_quantity = decimal.Decimal(\"3\")\n    sell_target = 3\n    buy_price = decimal.Decimal(\"2525\")\n    order_id_2 = \"b\"\n    consumer.sell_targets_by_order_id[order_id_2] = sell_target\n    await producer._create_sell_order_if_enabled(order_id_2, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy * 2 * 2 - 2))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders if o.origin_price > 150)\n    assert total_sell_quantity == sell_quantity * 2\n\n    max_price = buy_price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[sell_target]\n    increment = (max_price - buy_price) / consumer.trading_mode.sell_orders_per_buy\n    limit_orders = [o for o in open_orders if isinstance(o, trading_personal_data.SellLimitOrder)]\n    stop_orders = [o for o in open_orders if isinstance(o, trading_personal_data.StopLossOrder)]\n    assert limit_orders[2 + 0].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + increment, 8)\n    assert limit_orders[2 + 1].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + 2 * increment, 8)\n    assert limit_orders[2 + 2].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + 3 * increment, 8)\n\n    # now fill a stop order\n    await _fill_order(stop_orders[-1], trader, trigger_update_callback=False, consumer=consumer, closed_orders_count=2)\n    # create as task to allow creator's queue to get processed\n    # associated sell order gets cancelled\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy * 2 * 2 - 2 * 2))\n\n\nasync def test_create_too_large_sell_orders(tools):\n    producer, consumer, trader = tools\n\n    # case 1: too many orders to create: problem\n    sell_quantity = decimal.Decimal(\"500000000\")\n    sell_target = 2\n    buy_price = decimal.Decimal(\"10000000\")\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = sell_quantity\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = sell_quantity\n    order_id = \"a\"\n    consumer.sell_targets_by_order_id[order_id] = sell_target\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 0))\n\n    # case 2: create split sell orders\n    sell_quantity = decimal.Decimal(\"5000000\")\n    buy_price = decimal.Decimal(\"3000000\")\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    for _ in range(17):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, 17))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    # rounding because orders to create volumes are with truncated decimals\n    assert sell_quantity * decimal.Decimal(\"0.9999\") <= total_sell_quantity <= sell_quantity\n\n    max_price = buy_price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[sell_target]\n    increment = (max_price - buy_price) / 17\n    assert open_orders[0].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(buy_price + increment, 8)\n    assert open_orders[-1].origin_price == max_price\n\n\nasync def test_create_too_small_sell_orders(tools):\n    producer, consumer, trader = tools\n\n    # case 1: not enough to create any order: problem\n    sell_quantity = decimal.Decimal(\"0.001\")\n    sell_target = 2\n    buy_price = decimal.Decimal(\"0.001\")\n    order_id = \"a\"\n    consumer.sell_targets_by_order_id[order_id] = sell_target\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 0))\n\n    # case 2: create less than 3 orders: 1 order\n    sell_quantity = decimal.Decimal(\"0.1\")\n    buy_price = decimal.Decimal(\"0.01\")\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert len(open_orders) == 1\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    assert total_sell_quantity == sell_quantity\n\n    max_price = buy_price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[sell_target]\n    assert open_orders[0].origin_price == max_price\n\n    # case 3: create less than 3 orders: 2 orders\n    sell_quantity = decimal.Decimal(\"0.2\")\n    sell_target = 2\n    buy_price = decimal.Decimal(\"0.01\")\n    # keep same order id to test no issue with it\n    await producer._create_sell_order_if_enabled(order_id, sell_quantity, buy_price)\n    # create as task to allow creator's queue to get processed\n    for _ in range(3):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, 3))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    second_total_sell_quantity = sum(o.origin_quantity for o in open_orders if o.origin_price >= 0.0107)\n    assert decimal.Decimal(f\"{second_total_sell_quantity}\") == sell_quantity\n\n    max_price = buy_price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[sell_target]\n    increment = (max_price - buy_price) / 2\n    assert open_orders[1].origin_price == buy_price + increment\n    assert open_orders[2].origin_price == max_price\n\n\nasync def test_order_fill_callback_with_limit_entry(tools):\n    producer, consumer, trader = tools\n\n    volume_weight = 1\n    price_weight = 1\n    await producer._create_bottom_order(1, volume_weight, price_weight)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n\n    # change weights to ensure no interference\n    volume_weight = 3\n    price_weight = 3\n\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    to_fill_order = open_orders[0]\n    await _fill_order(to_fill_order, trader, consumer=consumer)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy))\n\n    assert to_fill_order.status == trading_enums.OrderStatus.FILLED\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    assert to_fill_order.origin_quantity * decimal.Decimal(\"0.95\") <= total_sell_quantity <= to_fill_order.origin_quantity\n\n    price = decimal.Decimal(f\"{to_fill_order.filled_price}\")\n    max_price = price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[1]\n    increment = (max_price - price) / consumer.trading_mode.sell_orders_per_buy\n    assert open_orders[0].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + increment, 8)\n    assert open_orders[1].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + 2 * increment, 8)\n    assert open_orders[2].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + 3 * increment, 8)\n\n    # now fill a sell order\n    await _fill_order(open_orders[0], trader, consumer=consumer)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy - 1))\n\n    # new buy order\n    await producer._create_bottom_order(2, volume_weight, price_weight)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy))\n\n\nasync def test_order_fill_callback_with_market_entry(tools):\n    producer, consumer, trader = tools\n\n    volume_weight = 1\n    price_weight = 1\n    consumer.USE_BUY_MARKET_ORDERS_VALUE = True\n    await producer._create_bottom_order(1, volume_weight, price_weight)\n    # create as task to allow creator's queue to get processed\n    # market order is instantly filled\n    await asyncio.create_task(_check_open_orders_count(trader, 0))\n\n    entry = trading_api.get_trade_history(trader.exchange_manager)[0]\n\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy))\n\n    assert entry.status == trading_enums.OrderStatus.FILLED\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    assert entry.origin_quantity * decimal.Decimal(\"0.95\") <= total_sell_quantity <= entry.origin_quantity\n\n    price = decimal.Decimal(f\"{entry.executed_price}\")\n    max_price = price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[1]\n    increment = (max_price - price) / consumer.trading_mode.sell_orders_per_buy\n    assert open_orders[0].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + increment, 8)\n    assert open_orders[1].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + 2 * increment, 8)\n    assert open_orders[2].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + 3 * increment, 8)\n\n\nasync def test_order_fill_callback_without_fees(tools):\n    producer, consumer, trader = tools\n\n    producer.ignore_exchange_fees = True\n\n    volume_weight = 1\n    price_weight = 1\n    await producer._create_bottom_order(1, volume_weight, price_weight)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    to_fill_order = open_orders[0]\n    await _fill_order(to_fill_order, trader, consumer=consumer)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy))\n\n    assert to_fill_order.status == trading_enums.OrderStatus.FILLED\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    assert total_sell_quantity == to_fill_order.origin_quantity\n\n\nasync def test_order_fill_callback_without_fees_adapted_rounding(tools):\n    producer, consumer, trader = tools\n\n    producer.ignore_exchange_fees = True\n\n    volume_weight = 1\n    price_weight = 1\n    await producer._create_bottom_order(1, volume_weight, price_weight)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    to_fill_order = open_orders[0]\n    to_fill_order.origin_quantity = decimal.Decimal(\"0.000167\")\n    to_fill_order.origin_price = decimal.Decimal(\"200\")\n\n    await _fill_order(to_fill_order, trader, consumer=consumer)\n    # create as task to allow creator's queue to get processed\n    for _ in range(consumer.trading_mode.sell_orders_per_buy):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, consumer.trading_mode.sell_orders_per_buy))\n\n    assert to_fill_order.status == trading_enums.OrderStatus.FILLED\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    assert total_sell_quantity == to_fill_order.origin_quantity\n\n\nasync def test_order_fill_callback_not_in_db(tools):\n    producer, consumer, trader = tools\n\n    await producer._create_bottom_order(2, 1, 1)\n    # create as task to allow creator's queue to get processed\n    await asyncio.create_task(_check_open_orders_count(trader, 1))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n    to_fill_order = open_orders[0]\n    await _fill_order(to_fill_order, trader, trigger_update_callback=False, consumer=consumer)\n\n    # remove order from db\n    consumer.sell_targets_by_order_id = {}\n    await consumer.trading_mode._order_notification_callback(None,\n                                                             trader.exchange_manager.id,\n                                                             None,\n                                                             symbol=to_fill_order.symbol,\n                                                             order=to_fill_order.to_dict(),\n                                                             update_type=trading_enums.OrderUpdateType.STATE_CHANGE.value,\n                                                             is_from_bot=True)\n    # create as task to allow creator's queue to get processed\n    for _ in range(3):\n        await asyncio_tools.wait_asyncio_next_cycle()\n    await asyncio.create_task(_check_open_orders_count(trader, 3))\n    open_orders = trading_api.get_open_orders(trader.exchange_manager)\n\n    assert all(o.status == trading_enums.OrderStatus.OPEN for o in open_orders)\n    assert all(o.side == trading_enums.TradeOrderSide.SELL for o in open_orders)\n    total_sell_quantity = sum(o.origin_quantity for o in open_orders)\n    assert to_fill_order.origin_quantity * decimal.Decimal(\"0.95\") <= total_sell_quantity <= to_fill_order.origin_quantity\n\n    price = decimal.Decimal(to_fill_order.filled_price)\n    max_price = price * consumer.PRICE_WEIGH_TO_PRICE_PERCENT[consumer.DEFAULT_SELL_TARGET]\n    increment = (max_price - price) / consumer.trading_mode.sell_orders_per_buy\n    assert open_orders[0].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + increment, 8)\n    assert open_orders[1].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + 2 * increment, 8)\n    assert open_orders[2].origin_price == \\\n        trading_personal_data.decimal_trunc_with_n_decimal_digits(price + 3 * increment, 8)\n\n\nasync def _check_open_orders_count(trader, count):\n    assert len(trading_api.get_open_orders(trader.exchange_manager)) == count\n\n\nasync def _get_tools(symbol=\"BTC/USDT\"):\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n    exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n    exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n    # use backtesting not to spam exchanges apis\n    exchange_manager.is_simulated = True\n    exchange_manager.is_backtesting = True\n    exchange_manager.use_cached_markets = False\n    backtesting = await backtesting_api.initialize_backtesting(\n        config,\n        exchange_ids=[exchange_manager.id],\n        matrix_id=None,\n        data_files=[os.path.join(test_config.TEST_CONFIG_FOLDER,\n                                 \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n    exchange_manager.exchange = exchanges.ExchangeSimulator(\n        exchange_manager.config, exchange_manager, backtesting\n    )\n    await exchange_manager.exchange.initialize()\n    for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n        await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                         exchange_manager=exchange_manager)\n\n    trader = exchanges.TraderSimulator(config, exchange_manager)\n    await trader.initialize()\n\n    mode = Mode.DipAnalyserTradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    await mode.initialize()\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n\n    # set BTC/USDT price at 1000 USDT\n    trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n    return mode.producers[0], mode.get_trading_mode_consumers()[0], trader\n\n\nasync def _fill_order(order, trader, trigger_update_callback=True, ignore_open_orders=False, consumer=None,\n                      closed_orders_count=1):\n    initial_len = len(trading_api.get_open_orders(trader.exchange_manager))\n    await order.on_fill(force_fill=True)\n    if order.status == trading_enums.OrderStatus.FILLED:\n        if not ignore_open_orders:\n            assert len(trading_api.get_open_orders(trader.exchange_manager)) == initial_len - closed_orders_count\n        if trigger_update_callback:\n            await asyncio_tools.wait_asyncio_next_cycle()\n        else:\n            with mock.patch.object(consumer, \"create_new_orders\", new=mock.AsyncMock()):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/__init__.py",
    "content": "from .grid_trading import GridTradingMode"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/config/GridTradingMode.json",
    "content": "{\n  \"required_strategies\": [],\n  \"pair_settings\": [\n    {\n      \"pair\": \"BTC/USDT\",\n      \"flat_spread\": 2000,\n      \"flat_increment\": 1000,\n      \"buy_orders_count\": 25,\n      \"sell_orders_count\": 25,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"reinvest_profits\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": false,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    },\n    {\n      \"pair\": \"ADA/ETH\",\n      \"flat_spread\": 0.00002,\n      \"flat_increment\": 0.00001,\n      \"buy_orders_count\": 25,\n      \"sell_orders_count\": 25,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"reinvest_profits\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": false,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    },\n    {\n      \"pair\": \"ETH/USDT\",\n      \"flat_spread\": 10,\n      \"flat_increment\": 5,\n      \"buy_orders_count\": 25,\n      \"sell_orders_count\": 25,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"reinvest_profits\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": false,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    }\n  ]\n}"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/grid_trading.py",
    "content": "# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport dataclasses\nimport decimal\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_trading.api as trading_api\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.errors as trading_errors\nimport tentacles.Trading.Mode.staggered_orders_trading_mode.staggered_orders_trading as staggered_orders_trading\n\n\n@dataclasses.dataclass\nclass AllowedPriceRange:\n    lower_bound: decimal.Decimal = trading_constants.ZERO\n    higher_bound: decimal.Decimal = trading_constants.ZERO\n\n\nclass GridTradingMode(staggered_orders_trading.StaggeredOrdersTradingMode):\n    CONFIG_FLAT_SPREAD = \"flat_spread\"\n    CONFIG_FLAT_INCREMENT = \"flat_increment\"\n    CONFIG_BUY_ORDERS_COUNT = \"buy_orders_count\"\n    CONFIG_SELL_ORDERS_COUNT = \"sell_orders_count\"\n    LIMIT_ORDERS_IF_NECESSARY = \"limit_orders_if_necessary\"\n    USER_COMMAND_CREATE_ORDERS = \"create initial orders\"\n    USER_COMMAND_STOP_ORDERS_CREATION = \"stop initial orders creation\"\n    USER_COMMAND_PAUSE_ORDER_MIRRORING = \"pause orders mirroring\"\n    USER_COMMAND_TRADING_PAIR = \"trading pair\"\n    USER_COMMAND_PAUSE_TIME = \"pause length in seconds\"\n    SUPPORTS_HEALTH_CHECK = False   # WIP   # set True when self.health_check is implemented\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        default_config = self.get_default_pair_config(\n            \"BTC/USDT\", 0.05, 0.005,\n            None, None, None, None, None\n        )\n        self.UI.user_input(self.CONFIG_PAIR_SETTINGS, commons_enums.UserInputTypes.OBJECT_ARRAY,\n                           self.trading_config.get(self.CONFIG_PAIR_SETTINGS, None), inputs,\n                           item_title=\"Pair configuration\",\n                           other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n                           title=\"Configuration for each traded pairs.\")\n        self.UI.user_input(self.CONFIG_PAIR, commons_enums.UserInputTypes.TEXT,\n                           default_config[self.CONFIG_PAIR], inputs,\n                           other_schema_values={\"minLength\": 3, \"pattern\": commons_constants.TRADING_SYMBOL_REGEX},\n                           parent_input_name=self.CONFIG_PAIR_SETTINGS,\n                           title=\"Name of the traded pair.\")\n        self.UI.user_input(\n            self.CONFIG_FLAT_SPREAD, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_FLAT_SPREAD], inputs,\n            min_val=0, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Spread: price difference between the closest buy and sell orders. Denominated in the quote currency \"\n                  \"(600 for a 600 USDT spread on BTC/USDT).\",\n        )\n        self.UI.user_input(\n            self.CONFIG_FLAT_INCREMENT, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_FLAT_INCREMENT], inputs,\n            min_val=0, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Increment: price difference between two orders of the same side. Denominated in the quote currency \"\n                  \"(200 for a 200 USDT spread on BTC/USDT). \"\n                  \"WARNING: this should be lower than the Spread value: profitability is close to Spread-Increment.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_BUY_ORDERS_COUNT, commons_enums.UserInputTypes.INT,\n            default_config[self.CONFIG_BUY_ORDERS_COUNT], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Buy orders count: number of initial buy orders to create. Make sure to have enough funds \"\n                  \"to create that many orders.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_SELL_ORDERS_COUNT, commons_enums.UserInputTypes.INT,\n            default_config[self.CONFIG_SELL_ORDERS_COUNT], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Sell orders count: Number of initial sell orders to create. Make sure to have enough funds \"\n                  \"to create that many orders.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_BUY_FUNDS, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_BUY_FUNDS], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Total buy funds: total funds to use for buy orders creation. \"\n                  \"Denominated in quote currency: enter 1000 to create a grid on BTC/USDT using up to a total of 1000 \"\n                  \"USDT in its buy orders. Set 0 to use all available funds in portfolio. \"\n                  \"A value is required to use the same currency simultaneously in multiple traded pairs.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_SELL_FUNDS, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_SELL_FUNDS], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Total sell funds: total funds to use for sell orders creation. \"\n                  \"Denominated in base currency: enter 0.01 to create a grid on BTC/USDT using up to a total of 0.01 \"\n                  \"BTC in its sell orders. Set 0 to use all available funds in portfolio. \"\n                  \"A value is required to use the same currency simultaneously in multiple traded pairs.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_STARTING_PRICE, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_STARTING_PRICE], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Starting price: price to compute initial orders from. Set 0 to use current \"\n                  \"exchange price during initial grid orders creation.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_BUY_VOLUME_PER_ORDER, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_BUY_VOLUME_PER_ORDER], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Buy orders volume: volume of each buy order in base currency. Set 0 to use all \"\n                  \"available funds in portfolio (or total buy funds if set) and create orders with constant \"\n                  \"total order cost (price * volume).\",\n        )\n        self.UI.user_input(\n            self.CONFIG_SELL_VOLUME_PER_ORDER, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_SELL_VOLUME_PER_ORDER], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Sell orders volume: volume of each sell order in base currency. Set 0 to use all \"\n                  \"available funds in portfolio (or total sell funds if set) and create orders with constant \"\n                  \"total order cost (price * volume).\",\n        )\n        self.UI.user_input(\n            self.CONFIG_IGNORE_EXCHANGE_FEES, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_IGNORE_EXCHANGE_FEES], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Ignore exchange fees: when checked, exchange fees won't be considered when creating mirror orders. \"\n                  \"When unchecked, a part of the total volume will be reduced to take exchange \"\n                  \"fees into account.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_MIRROR_ORDER_DELAY, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_MIRROR_ORDER_DELAY], inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Mirror order delay: Seconds to wait for before creating a mirror order when an order \"\n                  \"is filled. This can generate extra profits on quick market moves.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_USE_EXISTING_ORDERS_ONLY, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_USE_EXISTING_ORDERS_ONLY], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Use existing orders only: when checked, new orders will only be created upon pre-existing orders \"\n                  \"fill. OctoBot won't create orders at startup: it will use the ones already on exchange instead. \"\n                  \"This mode allows grid orders to operate on user created orders. Can't work on trading simulator.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_ENABLE_TRAILING_UP, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_ENABLE_TRAILING_UP], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Trailing up: when checked, the whole grid will be cancelled and recreated when price goes above the \"\n                  \"highest selling price. This might require the grid to perform a buy market order to be \"\n                  \"able to recreate the grid new sell orders at the updated price.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_ENABLE_TRAILING_DOWN, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_ENABLE_TRAILING_DOWN], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Trailing down: when checked, the whole grid will be cancelled and recreated when price goes bellow\"\n                  \" the lowest buying price. This might require the grid to perform a sell market order to be \"\n                  \"able to recreate the grid new buy orders at the updated price. \"\n                  \"Warning: when trailing down, the sell order required to recreate the buying side of the grid \"\n                  \"might generate a loss.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_ENABLE_TRAILING_UP, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_ENABLE_TRAILING_UP], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Trailing up: when checked, the whole grid will be cancelled and recreated when price goes above the \"\n                  \"highest selling price. This might require the grid to perform a buy market order to be \"\n                  \"able to recreate the grid new sell orders at the updated price.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_ORDER_BY_ORDER_TRAILING, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_ORDER_BY_ORDER_TRAILING], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Order by order trailing: when checked, the grid will trail order by order instead of the whole grid at once, which is adapted to less volatile markets.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_ALLOW_FUNDS_REDISPATCH, commons_enums.UserInputTypes.BOOLEAN,\n            default_config[self.CONFIG_ALLOW_FUNDS_REDISPATCH], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Auto-dispatch new funds: when checked, new available funds will be dispatched into existing \"\n                  \"orders when additional funds become available. Funds redispatch check happens once a day \"\n                  \"around your OctoBot start time.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_FUNDS_REDISPATCH_INTERVAL, commons_enums.UserInputTypes.FLOAT,\n            default_config[self.CONFIG_FUNDS_REDISPATCH_INTERVAL], inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Auto-dispatch interval: hours between each funds redispatch check.\",\n            editor_options={\n                commons_enums.UserInputOtherSchemaValuesTypes.DEPENDENCIES.value: {\n                  self.CONFIG_ALLOW_FUNDS_REDISPATCH: True\n                }\n            }\n        )\n\n    @classmethod\n    def get_default_pair_config(\n        cls, symbol, flat_spread: float, flat_increment: float,\n        buy_count: typing.Optional[int], sell_count: typing.Optional[int],\n        enable_trailing_up: typing.Optional[bool], enable_trailing_down: typing.Optional[bool],\n        order_by_order_trailing: typing.Optional[bool]\n    ) -> dict:\n        return {\n          cls.CONFIG_PAIR: symbol,\n          cls.CONFIG_FLAT_SPREAD: flat_spread,\n          cls.CONFIG_FLAT_INCREMENT: flat_increment,\n          cls.CONFIG_BUY_ORDERS_COUNT: buy_count or 20,\n          cls.CONFIG_SELL_ORDERS_COUNT: sell_count or 20,\n          cls.CONFIG_SELL_FUNDS: 0,\n          cls.CONFIG_BUY_FUNDS: 0,\n          cls.CONFIG_STARTING_PRICE: 0,\n          cls.CONFIG_BUY_VOLUME_PER_ORDER: 0,\n          cls.CONFIG_SELL_VOLUME_PER_ORDER: 0,\n          cls.CONFIG_IGNORE_EXCHANGE_FEES: True,\n          cls.CONFIG_MIRROR_ORDER_DELAY: 0,\n          cls.CONFIG_USE_EXISTING_ORDERS_ONLY: False,\n          cls.CONFIG_ALLOW_FUNDS_REDISPATCH: False,\n          cls.CONFIG_ENABLE_TRAILING_UP: enable_trailing_up or False,\n          cls.CONFIG_ENABLE_TRAILING_DOWN: enable_trailing_down or False,\n          # enabled by default\n          cls.CONFIG_ORDER_BY_ORDER_TRAILING: True if order_by_order_trailing is None else order_by_order_trailing,\n          cls.CONFIG_FUNDS_REDISPATCH_INTERVAL: 24,\n        }\n\n    def get_mode_producer_classes(self) -> list:\n        return [GridTradingModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [GridTradingModeConsumer]\n\n    async def user_commands_callback(self, bot_id, subject, action, data) -> None:\n        await super().user_commands_callback(bot_id, subject, action, data)\n        if data and data.get(GridTradingMode.USER_COMMAND_TRADING_PAIR, \"\").upper() == self.symbol:\n            self.logger.info(f\"Received {action} command for {self.symbol}.\")\n            if action == GridTradingMode.USER_COMMAND_CREATE_ORDERS:\n                await self.producers[0].trigger_staggered_orders_creation()\n            elif action == GridTradingMode.USER_COMMAND_STOP_ORDERS_CREATION:\n                await self.get_trading_mode_consumers()[0].cancel_orders_creation()\n            elif action == GridTradingMode.USER_COMMAND_PAUSE_ORDER_MIRRORING:\n                delay = float(data.get(GridTradingMode.USER_COMMAND_PAUSE_TIME, 0))\n                self.producers[0].start_mirroring_pause(delay)\n\n    @classmethod\n    def get_user_commands(cls) -> dict:\n        \"\"\"\n        Return the dict of user commands for this tentacle\n        :return: the commands dict\n        \"\"\"\n        return {\n            **super().get_user_commands(),\n            **{\n                GridTradingMode.USER_COMMAND_CREATE_ORDERS: {\n                    GridTradingMode.USER_COMMAND_TRADING_PAIR: \"text\"\n                },\n                GridTradingMode.USER_COMMAND_STOP_ORDERS_CREATION: {\n                    GridTradingMode.USER_COMMAND_TRADING_PAIR: \"text\"\n                },\n                GridTradingMode.USER_COMMAND_PAUSE_ORDER_MIRRORING: {\n                    GridTradingMode.USER_COMMAND_TRADING_PAIR: \"text\",\n                    GridTradingMode.USER_COMMAND_PAUSE_TIME: \"number\"\n                }\n            }\n        }\n\n\nclass GridTradingModeConsumer(staggered_orders_trading.StaggeredOrdersTradingModeConsumer):\n    pass\n\n\nclass GridTradingModeProducer(staggered_orders_trading.StaggeredOrdersTradingModeProducer):\n    # Disable health check\n    HEALTH_CHECK_INTERVAL_SECS = None\n    ORDERS_DESC = \"grid\"\n    RECENT_TRADES_ALLOWED_TIME = 2 * commons_constants.DAYS_TO_SECONDS\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        self.buy_orders_count = self.sell_orders_count = None\n        self.sell_price_range = AllowedPriceRange()\n        self.buy_price_range = AllowedPriceRange()\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        self._expect_missing_orders = True\n        self._skip_order_restore_on_recently_closed_orders = False\n        self._use_recent_trades_for_order_restore = True\n        self.allow_virtual_orders = False\n\n    def read_config(self):\n        self.mode = staggered_orders_trading.StrategyModes.FLAT\n        # init decimals from str to remove native float rounding\n        self.flat_spread = None if self.symbol_trading_config[self.trading_mode.CONFIG_FLAT_SPREAD] is None \\\n            else decimal.Decimal(str(self.symbol_trading_config[self.trading_mode.CONFIG_FLAT_SPREAD]))\n        self.flat_increment = None if self.symbol_trading_config[self.trading_mode.CONFIG_FLAT_INCREMENT] is None \\\n            else decimal.Decimal(str(self.symbol_trading_config[self.trading_mode.CONFIG_FLAT_INCREMENT]))\n        # decimal.Decimal operations are supporting int values, no need to convert these into decimal.Decimal\n        self.buy_orders_count = self.symbol_trading_config[self.trading_mode.CONFIG_BUY_ORDERS_COUNT]\n        self.sell_orders_count = self.symbol_trading_config[self.trading_mode.CONFIG_SELL_ORDERS_COUNT]\n        self.operational_depth = self.buy_orders_count + self.sell_orders_count\n        self.buy_funds = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_BUY_FUNDS,\n                                                                            self.buy_funds)))\n        self.sell_funds = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_SELL_FUNDS,\n                                                                             self.sell_funds)))\n        self.starting_price = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_STARTING_PRICE,\n                                                                                 self.starting_price)))\n        self.sell_volume_per_order = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_SELL_VOLUME_PER_ORDER,\n                                                                                        self.sell_volume_per_order)))\n        self.buy_volume_per_order = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_BUY_VOLUME_PER_ORDER,\n                                                                                       self.buy_volume_per_order)))\n        self.limit_orders_count_if_necessary = \\\n            self.symbol_trading_config.get(self.trading_mode.LIMIT_ORDERS_IF_NECESSARY, True)\n        self.ignore_exchange_fees = self.symbol_trading_config.get(self.trading_mode.CONFIG_IGNORE_EXCHANGE_FEES,\n                                                                   self.ignore_exchange_fees)\n        self.use_existing_orders_only = self.symbol_trading_config.get(self.trading_mode.CONFIG_USE_EXISTING_ORDERS_ONLY,\n                                                                       self.use_existing_orders_only)\n        self.mirror_order_delay = self.symbol_trading_config.get(self.trading_mode.CONFIG_MIRROR_ORDER_DELAY,\n                                                                 self.mirror_order_delay)\n        self.allow_order_funds_redispatch = self.symbol_trading_config.get(\n            self.trading_mode.CONFIG_ALLOW_FUNDS_REDISPATCH, self.allow_order_funds_redispatch\n        )\n        if self.allow_order_funds_redispatch:\n            # check every day that funds should not be redispatched and of orders are missing\n            self.health_check_interval_secs = self.symbol_trading_config.get(\n                self.trading_mode.CONFIG_FUNDS_REDISPATCH_INTERVAL, self.funds_redispatch_interval\n            ) * commons_constants.HOURS_TO_SECONDS\n        self.enable_trailing_up = self.symbol_trading_config.get(\n            self.trading_mode.CONFIG_ENABLE_TRAILING_UP, self.enable_trailing_up\n        )\n        self.enable_trailing_down = self.symbol_trading_config.get(\n            self.trading_mode.CONFIG_ENABLE_TRAILING_DOWN, self.enable_trailing_down\n        )\n        self.use_order_by_order_trailing = self.symbol_trading_config.get(\n            self.trading_mode.CONFIG_ORDER_BY_ORDER_TRAILING, self.use_order_by_order_trailing\n        )\n        self.compensate_for_missed_mirror_order = self.symbol_trading_config.get(\n            self.trading_mode.COMPENSATE_FOR_MISSED_MIRROR_ORDER, self.use_order_by_order_trailing # use use_order_by_order_trailing as default value as compensate_for_missed_mirror_order is required for order by order trailing \n        )\n\n    async def _handle_staggered_orders(\n        self, current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing\n    ):\n        self._init_allowed_price_ranges(current_price)\n        if ignore_mirror_orders_only or not self.use_existing_orders_only:\n            async with self.producer_exchange_wide_lock(self.exchange_manager):\n                if trigger_trailing and self.is_currently_trailing:\n                    self.logger.debug(\n                        f\"{self.symbol} on {self.exchange_name}: trailing signal ignored: \"\n                        f\"a trailing process is already running\"\n                    )\n                    return\n                # use exchange level lock to prevent funds double spend\n                buy_orders, sell_orders, triggering_trailing, create_order_dependencies = await self._generate_staggered_orders(\n                    current_price, ignore_available_funds, trigger_trailing\n                )\n                grid_orders = self._merged_and_sort_not_virtual_orders(buy_orders, sell_orders)\n                await self._create_not_virtual_orders(\n                    grid_orders, current_price, triggering_trailing, create_order_dependencies\n                )\n                if grid_orders:\n                    self._already_created_init_orders = True\n\n    async def trigger_staggered_orders_creation(self):\n        # reload configuration\n        await self.trading_mode.reload_config(self.exchange_manager.bot_id)\n        self._load_symbol_trading_config()\n        self.read_config()\n        if self.symbol_trading_config:\n            await self._ensure_staggered_orders(ignore_mirror_orders_only=True, ignore_available_funds=True)\n        else:\n            self.logger.error(f\"No configuration for {self.symbol}\")\n\n    def _load_symbol_trading_config(self) -> bool:\n        if not super()._load_symbol_trading_config():\n            return self._apply_default_symbol_config()\n        return True\n\n    def _apply_default_symbol_config(self) -> bool:\n        if not self.trading_mode.trading_config.get(commons_constants.ALLOW_DEFAULT_CONFIG, True):\n            raise trading_errors.TradingModeIncompatibility(\n                f\"{self.trading_mode.get_name()} default configuration is not allowed. \"\n                f\"Please configure the {self.symbol} settings.\"\n            )\n        self.logger.info(f\"Using default configuration for {self.symbol} as no configuration \"\n                         f\"is specified for this pair.\")\n        # set spread and increment as multipliers of the current price\n        self.spread = decimal.Decimal(str(self.trading_mode.CONFIG_DEFAULT_SPREAD_PERCENT / 100))\n        self.increment = decimal.Decimal(str(self.trading_mode.CONFIG_DEFAULT_INCREMENT_PERCENT / 100))\n        self.symbol_trading_config = self.trading_mode.get_default_pair_config(\n            self.symbol,\n            None,   # will compute flat_spread from self.spread\n            None,   # will compute flat_increment from self.increment\n            None,\n            None,\n            None,\n            None,\n            None\n        )\n        return True\n\n    async def _generate_staggered_orders(self, current_price, ignore_available_funds, trigger_trailing):\n        order_manager = self.exchange_manager.exchange_personal_data.orders_manager\n        if not self.single_pair_setup:\n            interfering_orders_pairs = self._get_interfering_orders_pairs(order_manager.get_open_orders())\n            if interfering_orders_pairs:\n                self.logger.error(\n                    f\"Impossible to create {self.symbol} grid orders with open orders on \"\n                    f\"{', '.join(interfering_orders_pairs)}. To use shared base or quote currencies, \"\n                    f\"set 'Total buy funds' and 'Total sell funds' in your {self.trading_mode.get_name()} \"\n                    f\"{self.symbol} configuration.\"\n                )\n                return [], [], False, None\n        existing_orders = order_manager.get_open_orders(self.symbol)\n\n        sorted_orders = self._get_grid_trades_or_orders(existing_orders)\n        oldest_existing_order_creation_time = min(\n            order.creation_time for order in sorted_orders\n        ) if sorted_orders else 0\n        recent_trades_time = max(\n            trading_api.get_exchange_current_time(\n                self.exchange_manager\n            ) - self.RECENT_TRADES_ALLOWED_TIME,\n            oldest_existing_order_creation_time\n        )\n        # list of trades orders from the most recent one to the oldest one\n        recently_closed_trades = sorted([\n            trade\n            for trade in trading_api.get_trade_history(\n                self.exchange_manager, symbol=self.symbol, since=recent_trades_time\n            )\n            # non limit orders are not to be taken into account\n            if trade.trade_type in (trading_enums.TraderOrderType.BUY_LIMIT, trading_enums.TraderOrderType.SELL_LIMIT)\n        ], key=lambda t: -t.executed_time)\n\n        lowest_buy = max(trading_constants.ZERO, self.buy_price_range.lower_bound)\n        highest_buy = self.buy_price_range.higher_bound\n        lowest_sell = self.sell_price_range.lower_bound\n        highest_sell = self.sell_price_range.higher_bound\n        if sorted_orders:\n            if self._should_trigger_trailing(sorted_orders, current_price, False):\n                trigger_trailing = True\n            if self.use_order_by_order_trailing or not trigger_trailing:\n                # grid boundaries are required in order by order trailing\n                buy_orders = [order for order in sorted_orders if order.side == trading_enums.TradeOrderSide.BUY]\n                sell_orders = [order for order in sorted_orders if order.side == trading_enums.TradeOrderSide.SELL]\n                highest_buy = current_price\n                lowest_sell = current_price\n                origin_created_buy_orders_count, origin_created_sell_orders_count = self._get_origin_orders_count(\n                    recently_closed_trades, sorted_orders\n                )\n\n                min_max_total_order_price_delta = (\n                    self.flat_increment * (origin_created_buy_orders_count - 1 + origin_created_sell_orders_count - 1)\n                    + self.flat_increment\n                )\n                if buy_orders:\n                    lowest_buy = buy_orders[0].origin_price\n                    if not sell_orders:\n                        highest_buy = min(current_price, lowest_buy + min_max_total_order_price_delta)\n                        # buy orders only\n                        lowest_sell = highest_buy + self.flat_spread - self.flat_increment\n                        highest_sell = lowest_buy + min_max_total_order_price_delta + self.flat_spread - self.flat_increment\n                    else:\n                        # use only open order prices when possible\n                        _highest_sell = sell_orders[-1].origin_price\n                        highest_buy = min(current_price, _highest_sell - self.flat_spread + self.flat_increment)\n                if sell_orders:\n                    highest_sell = sell_orders[-1].origin_price\n                    if not buy_orders:\n                        lowest_sell = max(current_price, highest_sell - min_max_total_order_price_delta)\n                        # sell orders only\n                        lowest_buy = max(\n                            0, highest_sell - min_max_total_order_price_delta - self.flat_spread + self.flat_increment\n                        )\n                        highest_buy = lowest_sell - self.flat_spread + self.flat_increment\n                    else:\n                        # use only open order prices when possible\n                        _lowest_buy = buy_orders[0].origin_price\n                        lowest_sell = max(current_price, _lowest_buy - self.flat_spread + self.flat_increment)\n        next_step_dependencies = None\n        trailing_buy_orders = trailing_sell_orders = []\n        confirmed_trailing = False\n        # print(f\"{self.use_order_by_order_trailing=}\")\n        if trigger_trailing:\n            # trailing has no initial dependencies here\n            _, __, trailing_buy_orders, trailing_sell_orders, next_step_dependencies = await self._prepare_trailing(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, \n                current_price, None\n            )\n            confirmed_trailing = True\n            # trailing will cancel all orders: set state to NEW with no existing order\n            missing_orders, state, sorted_orders = None, self.NEW, []\n        else:\n            # no trailing, process normal analysis\n            missing_orders, state, _ = self._analyse_current_orders_situation(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_sell, current_price\n            )\n            if missing_orders:\n                self.logger.info(\n                    f\"{len(missing_orders)} missing {self.symbol} orders on {self.exchange_name}: {missing_orders}\"\n                )\n            elif sorted_orders:\n                self.logger.info(\n                    f\"All {len(sorted_orders)} out of {self.buy_orders_count + self.sell_orders_count} {self.symbol} \"\n                    f\"target orders are in place on {self.exchange_name}\"\n                )\n\n            await self._handle_missed_mirror_orders_fills(recently_closed_trades, missing_orders, current_price)\n        try:\n            if trailing_buy_orders or trailing_sell_orders:\n                buy_orders = trailing_buy_orders\n                sell_orders = trailing_sell_orders\n            else:\n                # apply state and (re)create missing orders\n                buy_orders = self._create_orders(lowest_buy, highest_buy,\n                                                trading_enums.TradeOrderSide.BUY, sorted_orders,\n                                                current_price, missing_orders, state, self.buy_funds, ignore_available_funds,\n                                                recently_closed_trades)\n                sell_orders = self._create_orders(lowest_sell, highest_sell,\n                                                trading_enums.TradeOrderSide.SELL, sorted_orders,\n                                                current_price, missing_orders, state, self.sell_funds, ignore_available_funds,\n                                                recently_closed_trades)\n\n                if state is self.FILL and not confirmed_trailing:\n                    # don't check used funds if trailing is active to avoid cancelling trading\n                    self._ensure_used_funds(buy_orders, sell_orders, sorted_orders, recently_closed_trades)\n                elif state is self.NEW:\n                    if trigger_trailing and not (buy_orders or sell_orders):\n                        self.logger.error(f\"Unhandled situation: no orders created for {self.symbol} with trigger_trailing={trigger_trailing}\")\n            create_order_dependencies = next_step_dependencies\n        except staggered_orders_trading.ForceResetOrdersException:\n            lowest_buy = max(trading_constants.ZERO, self.buy_price_range.lower_bound)\n            highest_buy = self.buy_price_range.higher_bound\n            lowest_sell = self.sell_price_range.lower_bound\n            highest_sell = self.sell_price_range.higher_bound\n            buy_orders, sell_orders, state, create_order_dependencies = await self._reset_orders(\n                sorted_orders, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds, next_step_dependencies\n            )\n            confirmed_trailing = False\n\n        return buy_orders, sell_orders, confirmed_trailing, create_order_dependencies\n\n    def _get_origin_orders_count(self, recent_trades, open_orders):\n        origin_created_buy_orders_count = self.buy_orders_count\n        origin_created_sell_orders_count = self.sell_orders_count\n        if recent_trades:\n            # in case all initial orders didn't get created, try to infer the original value from open orders and trades\n            buy_orders_count = len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY])\n            buy_trades_count = len([trade for trade in recent_trades if trade.side is trading_enums.TradeOrderSide.BUY])\n            origin_created_buy_orders_count = buy_orders_count + buy_trades_count\n            origin_created_sell_orders_count = (\n                len(open_orders) + len(recent_trades) - origin_created_buy_orders_count\n            )\n            if origin_created_buy_orders_count + origin_created_sell_orders_count > self.buy_orders_count + self.sell_orders_count:\n                # more orders than in config (usually because of trailing), use config values\n                origin_created_buy_orders_count = self.buy_orders_count\n                origin_created_sell_orders_count = self.sell_orders_count\n        return origin_created_buy_orders_count, origin_created_sell_orders_count\n\n    def _get_grid_trades_or_orders(self, trades_or_orders):\n        if not trades_or_orders:\n            return trades_or_orders\n        sorted_elements = sorted(trades_or_orders, key=lambda t: self.get_trade_or_order_price(t))\n        four = decimal.Decimal(\"4\")\n        increment_lower_bound = - self.flat_increment / four\n        increment_higher_bound = self.flat_increment / four\n        filtered_out_orders = []\n        for first_element_index in range(len(sorted_elements)):\n            grid_trades_or_orders = []\n            previous_element = None\n            first_sided_element_price = None\n            for trade_or_order in sorted_elements[first_element_index:]:\n                if first_sided_element_price is None:\n                    first_sided_element_price = self.get_trade_or_order_price(trade_or_order)\n                if previous_element is None:\n                    grid_trades_or_orders.append(trade_or_order)\n                else:\n                    if trade_or_order.side != previous_element.side:\n                        # reached other side: take spread into account\n                        first_sided_element_price += self.flat_spread\n                    delta_increment = (self.get_trade_or_order_price(trade_or_order) - first_sided_element_price) \\\n                        % self.flat_increment\n                    if (\n                        # delta is between -25%*increment and 25%*increment\n                        increment_lower_bound < delta_increment < increment_higher_bound\n                    ) or (\n                        # delta is between 75%*increment and increment\n                        self.flat_increment - increment_higher_bound < delta_increment < self.flat_increment\n                    ):\n                        grid_trades_or_orders.append(trade_or_order)\n                    else:\n                        filtered_out_orders.append(trade_or_order)\n                previous_element = trade_or_order\n            if filtered_out_orders:\n                self.logger.info(\n                    f\"Filtered out {len(filtered_out_orders)} {self.symbol} non grid orders out of \"\n                    f\"{len(trades_or_orders)} [{self.exchange_manager.exchange_name}] orders\"\n                )\n            if len(grid_trades_or_orders) / len(sorted_elements) > 0.5:\n                # make sure that we did not miss every grid trade by basing computations on a non grid trade\n                # more than 50% match of grid trades: grid trades are found\n                return grid_trades_or_orders\n        # grid trades are not found, use every trade\n        return sorted_elements\n\n    def _init_allowed_price_ranges(self, current_price):\n        self._set_increment_and_spread(current_price)\n        first_sell_price = current_price + (self.flat_spread / 2)\n        self.sell_price_range.higher_bound = first_sell_price + (self.flat_increment * (self.sell_orders_count - 1))\n        self.sell_price_range.lower_bound = max(current_price, first_sell_price)\n        first_buy_price = current_price - (self.flat_spread / 2)\n        self.buy_price_range.higher_bound = min(current_price, first_buy_price)\n        self.buy_price_range.lower_bound = first_buy_price - (self.flat_increment * (self.buy_orders_count - 1))\n\n    def _check_params(self):\n        if None not in (self.flat_increment, self.flat_spread) and self.flat_increment >= self.flat_spread:\n            self.logger.error(f\"Your flat_spread parameter should always be higher than your flat_increment\"\n                              f\" parameter: average profit is spread-increment. ({self.symbol})\")\n\n    def _create_new_orders_bundle(\n        self, lower_bound, upper_bound, side, current_price, allowed_funds, ignore_available_funds, selling,\n        order_limiting_currency, order_limiting_currency_amount\n    ):\n        orders = []\n        funds_to_use = self._get_maximum_traded_funds(allowed_funds,\n                                                      order_limiting_currency_amount,\n                                                      order_limiting_currency,\n                                                      selling,\n                                                      ignore_available_funds)\n        if funds_to_use == 0:\n            return []\n        starting_bound = lower_bound if selling else upper_bound\n        self._create_new_orders(orders, current_price, selling, lower_bound, upper_bound,\n                                funds_to_use, order_limiting_currency, starting_bound, side, False,\n                                self.mode, order_limiting_currency_amount)\n        return orders\n\n    def _get_order_count_and_average_quantity(self, current_price, selling, lower_bound, upper_bound, holdings,\n                                              currency, mode):\n        if lower_bound >= upper_bound:\n            self.logger.error(f\"Invalid bounds for {self.symbol}: too close to the current price\")\n            return 0, 0\n        orders_count = self.sell_orders_count if selling else self.buy_orders_count\n        if self._use_variable_orders_volume(trading_enums.TradeOrderSide.SELL if selling\n           else trading_enums.TradeOrderSide.BUY):\n            return self._ensure_average_order_quantity(orders_count, current_price, selling, holdings, currency, mode)\n        else:\n            return self._get_orders_count_from_fixed_volume(selling, current_price, holdings, orders_count)\n\n    def _get_max_theoretical_orders_count(self):\n        return self.buy_orders_count + self.sell_orders_count\n"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"GridTradingMode\"],\n  \"tentacles-requirements\": [\"staggered_orders_trading_mode\"]\n}"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/resources/GridTradingMode.md",
    "content": "Places a fixed amount of buy and sell orders at fixed intervals to profit from any market move. When an order is filled,\na mirror order is instantly created and generates profit when completed.\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/grid-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=GridTradingModeDocs\">\nfull Grid trading mode guide</a>.\n\n#### Default configuration\nWhen left unspecified for a trading pair, the grid will be initialized with a spread\nof 1.5% of the current price and an increment of 0.5% and a maximum of 20 buy and sell orders.\n\nWhen enough funds are available, the default configuration will result in:\n- Up to 20 buy order covering 99.25% to 89.5% of the current price\n- Up to 20 sell orders covering 100.75% to 110.5% of the current price \n\n#### Trading pair configuration\nYou can customize the grid for each trading pair. To configure a pair, enter:\n- The name of the pair \n- The interval between buy and sell (spread) \n- The interval between each order (increment)\n- The amount of initial buy and sell orders to create \n\n#### Trailing options\nA grid can only operate within its price range. However, when trailing options are enabled, \nthe whole grid can be automatically cancelled and recreated \nwhen the traded asset's price moves beyond the grid range. In this case, a market order can be executed in order to \nhave the necessary funds to create the grid buy and sell orders.\n\n#### Profits\nProfits will be made from price movements within the covered price area.  \nIt never \"sells at a loss\", but always at a profit, therefore OctoBot never cancels any orders when using the Grid Trading Mode.\n\nTo apply changes to the Grid Trading Mode settings, you will have to manually cancel orders and restart your OctoBot.  \nThis trading mode instantly places opposite side orders when an order is filled.\n\nThis trading mode has been made possible thanks to the support of PKBO & Calusari.\n\n_This trading mode supports PNL history._\n"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/grid_trading_mode/tests/open_orders_data.py",
    "content": "import json\n\nimport octobot_trading.personal_data as personal_data\nimport octobot_trading.storage as trading_storage\n\n\nasync def get_full_sol_usdt_open_orders(exchange_manager) -> list[personal_data.Order]:\n    order_data = json.loads(FULL_SOL_USDT_OPEN_ORDERS_DATA)\n    pending_groups = {}\n    orders = []\n    for o in order_data:\n        orders.append(await personal_data.create_order_from_order_storage_details(\n            trading_storage.orders_storage.from_order_document(o), exchange_manager, pending_groups\n        ))\n    return orders\n\n\n\nFULL_SOL_USDT_OPEN_ORDERS_DATA = \"\"\"\n[\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.236395,\n            \"exchange_id\": \"5f72d5ab-e6d7-468f-9161-7d3c3150329a\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"e2d61a49-f184-4cc8-9a1b-d1e2570346c5\",\n            \"is_active\": true,\n            \"price\": 149.093,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362439.478,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.24154,\n            \"exchange_id\": \"7b010951-d6e5-4bb2-9494-df1ad09c8931\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"b66efc84-a7ac-40a0-9504-8c6e1c6286f1\",\n            \"is_active\": true,\n            \"price\": 149.436,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362439.993,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.246685,\n            \"exchange_id\": \"6d6e907b-33d6-4d54-b0cf-fa17b5e75ff9\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"293e61bc-24b8-4dc1-a717-62137c66e2b2\",\n            \"is_active\": true,\n            \"price\": 149.779,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362440.537,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.25183,\n            \"exchange_id\": \"55acfd16-05b0-4992-bd14-66214aaa5be0\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"4afd159e-9ba2-4fd6-85ea-378be6610d9d\",\n            \"is_active\": true,\n            \"price\": 150.122,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362441.365,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.256975,\n            \"exchange_id\": \"9f3c2566-2c27-46bf-8e6d-7fb20ec12269\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"a5e1f296-92b0-4908-bd55-83022affb484\",\n            \"is_active\": true,\n            \"price\": 150.465,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362442.069,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.26212,\n            \"exchange_id\": \"c6264220-3ab7-4d24-96c5-727c8634965c\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"acbb27bd-8de6-4f5b-b53d-e2a529acceeb\",\n            \"is_active\": true,\n            \"price\": 150.808,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362442.74,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.267265,\n            \"exchange_id\": \"bce0b649-e629-4f2c-90f7-83d4e5424653\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"a7170333-1b62-40cc-b4e7-0acdc607c211\",\n            \"is_active\": true,\n            \"price\": 151.151,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362443.241,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.272425,\n            \"exchange_id\": \"0527e79c-86bc-4721-9af6-e3d51af2ed00\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"1733285b-6a7e-470e-b429-64751300bd81\",\n            \"is_active\": true,\n            \"price\": 151.495,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362443.768,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.27757,\n            \"exchange_id\": \"56ba40bd-f94d-4d9b-8d72-1edea1ee5903\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"34c8004b-0dbf-4930-97cf-ea369844ba1a\",\n            \"is_active\": true,\n            \"price\": 151.838,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362444.591,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.282715,\n            \"exchange_id\": \"cbde43d2-c98b-4c6d-982c-653bdfd3b2ed\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"1bdad673-d4c8-4d7b-973a-ac406e613433\",\n            \"is_active\": true,\n            \"price\": 152.181,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362445.101,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.28786,\n            \"exchange_id\": \"023d3cf7-3e0b-41c5-b39d-bbce2070810e\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"b0d75c90-ccc6-4e6f-9d3c-be89c7391239\",\n            \"is_active\": true,\n            \"price\": 152.524,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362445.6,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.293005,\n            \"exchange_id\": \"26cd9f3e-63c4-40ff-a5a7-7d07fe2b1e9a\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"4f40a061-cc2c-4bd5-a587-311064fbd886\",\n            \"is_active\": true,\n            \"price\": 152.867,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362446.093,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.29815,\n            \"exchange_id\": \"3b158e17-5957-4f06-95e0-70dcf0e41fc8\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"997fc0f4-6d1f-4549-9c49-6fc4ddeb1f3e\",\n            \"is_active\": true,\n            \"price\": 153.21,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362446.587,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.303295,\n            \"exchange_id\": \"75bd9860-211f-4654-a699-0429c646c5ac\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"160ff3c9-9c36-4f7e-a244-903bc07ceb77\",\n            \"is_active\": true,\n            \"price\": 153.553,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362447.177,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.30844,\n            \"exchange_id\": \"64c40497-14f9-47f7-a1cd-52561d7b8e43\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"72494344-7783-4bea-86d2-7def01b6943c\",\n            \"is_active\": true,\n            \"price\": 153.896,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362447.668,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.313585,\n            \"exchange_id\": \"2dfd619b-0348-4510-bbfb-5d0a13ff7092\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"33021a88-fc36-41c0-8bac-1ba7f5f41054\",\n            \"is_active\": true,\n            \"price\": 154.239,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362448.162,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.31873,\n            \"exchange_id\": \"371c2f07-c976-4f43-a8e8-b55f5c6e6b56\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"bc398948-e494-46ee-ac54-ce0321d73661\",\n            \"is_active\": true,\n            \"price\": 154.582,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362452.3,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.32389,\n            \"exchange_id\": \"27ea1b0e-f4b7-4926-833c-0fe4aa1384a0\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"028481c6-e2dd-4d27-b7ad-e8ac8948065b\",\n            \"is_active\": true,\n            \"price\": 154.926,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362453.138,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.329035,\n            \"exchange_id\": \"449f2062-ff20-4343-b349-ef7bb5e0b463\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"85565926-e992-41dd-a922-5528e9dc3cd6\",\n            \"is_active\": true,\n            \"price\": 155.269,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362453.65,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.33418,\n            \"exchange_id\": \"0f3b10d0-9843-47fa-b9f8-0865bc3b1387\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"c25e0844-f26e-4ab3-ae00-61d26310a9b6\",\n            \"is_active\": true,\n            \"price\": 155.612,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362454.145,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.339325,\n            \"exchange_id\": \"74f4555d-5fe3-40e4-a0a4-6d7c9b3cd317\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"fb8011f2-5ff4-4033-920f-d347c235050b\",\n            \"is_active\": true,\n            \"price\": 155.955,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362454.64,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.34447,\n            \"exchange_id\": \"bf533d78-4f40-4626-aeab-fea7a4d1849e\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"be9643ca-bb77-47e7-9ac4-b0b686dfa5d2\",\n            \"is_active\": true,\n            \"price\": 156.298,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362455.132,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.349615,\n            \"exchange_id\": \"66613fa4-4d2c-4e6e-8908-05eda8ae8183\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"ae535faa-e4dd-4702-81f3-520a35059f41\",\n            \"is_active\": true,\n            \"price\": 156.641,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362455.629,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.35476,\n            \"exchange_id\": \"644cb5f8-dc8e-4ec0-a709-1d94901a2ac1\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"ae1ba3d9-bdbd-404e-9d15-ba97670ec8a7\",\n            \"is_active\": true,\n            \"price\": 156.984,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362456.128,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.359905,\n            \"exchange_id\": \"ce2d07d4-dde3-442b-923c-6490eeb5607d\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"b597f05a-11bf-4cf4-80ae-942b9dae6284\",\n            \"is_active\": true,\n            \"price\": 157.327,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"sell\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362456.625,\n            \"triggerAbove\": true,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.22567,\n            \"exchange_id\": \"cad8be3a-23ed-4d5e-ac4f-880123393a42\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"5c2eb7aa-49ac-4af5-b95b-f66d60f93197\",\n            \"is_active\": true,\n            \"price\": 148.378,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362457.121,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.015,\n            \"broker_applied\": false,\n            \"cost\": 2.220525,\n            \"exchange_id\": \"19dc8ee4-0dbb-4d39-ae65-400fcde7c5d2\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"bffaec37-383e-4fe1-ad9a-1514e23ee0a9\",\n            \"is_active\": true,\n            \"price\": 148.035,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362457.628,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.363072,\n            \"exchange_id\": \"d77e3751-55cf-4769-ab86-14a855fd0993\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"e885a482-4a5c-4b23-90eb-6ba44cea132f\",\n            \"is_active\": true,\n            \"price\": 147.692,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362458.122,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.357584,\n            \"exchange_id\": \"debe8041-2485-4f95-b1ca-13ed5f398933\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"3998e406-1f08-4c9b-9678-fb1bc256469a\",\n            \"is_active\": true,\n            \"price\": 147.349,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362458.623,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.352096,\n            \"exchange_id\": \"0148bdf1-088c-4d69-8d70-be4d0785ae62\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"3afaa44d-fba4-449a-9b1a-14b0cd2a654b\",\n            \"is_active\": true,\n            \"price\": 147.006,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362459.14,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.346608,\n            \"exchange_id\": \"a9ff2a8e-b9b8-48c3-9813-e5b592c4e0e3\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"c26fd512-b6c0-4bd4-90fd-3564e1c8f32c\",\n            \"is_active\": true,\n            \"price\": 146.663,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362459.638,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.34112,\n            \"exchange_id\": \"039e1b2a-b531-420a-8057-dcc4323ed63e\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"d0b68aea-521f-4019-9d71-554ccf3032a8\",\n            \"is_active\": true,\n            \"price\": 146.32,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362460.135,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.335616,\n            \"exchange_id\": \"3e907299-7cf3-47c1-bcb3-9b047453e917\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"dcdca84c-7636-4b4f-a32c-7c2f5f45ec10\",\n            \"is_active\": true,\n            \"price\": 145.976,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362460.624,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.330128,\n            \"exchange_id\": \"0eaad5e3-5158-4c90-b002-1a01b3d55704\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"172139ab-0841-4a02-b92a-6002fc31ba08\",\n            \"is_active\": true,\n            \"price\": 145.633,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362462.833,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.32464,\n            \"exchange_id\": \"696abfb4-df1c-44cc-93c2-6512fd787feb\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"cf223507-c436-4e65-94de-174e379d661d\",\n            \"is_active\": true,\n            \"price\": 145.29,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362463.354,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.319152,\n            \"exchange_id\": \"ba69fedb-623b-40a1-be23-f2ba1037e77a\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"2a9e57ed-531a-4649-a880-dff4513d9097\",\n            \"is_active\": true,\n            \"price\": 144.947,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362463.864,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.313664,\n            \"exchange_id\": \"cf9b9e19-ff79-4800-b81d-a0eaa44b1e2c\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"6473cef5-0f83-431d-9f7c-b7353456a484\",\n            \"is_active\": true,\n            \"price\": 144.604,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362464.381,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.308176,\n            \"exchange_id\": \"77a8433a-6ff7-4b2d-a640-b5e4f592eb3e\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"a0407fb5-599f-4d88-ae08-2d678ca375cb\",\n            \"is_active\": true,\n            \"price\": 144.261,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362464.869,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.302688,\n            \"exchange_id\": \"0f9a422f-7f69-4c8a-8649-f451441d588f\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"b8668c80-b436-4551-91b1-669fae2996e9\",\n            \"is_active\": true,\n            \"price\": 143.918,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362465.362,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.2972,\n            \"exchange_id\": \"9f558a98-44c3-49d3-bb16-a75e5d294c24\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"c6b4abd4-e419-424e-8726-5541856b7a8f\",\n            \"is_active\": true,\n            \"price\": 143.575,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362465.858,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.291712,\n            \"exchange_id\": \"5c6a4c92-b7b3-425c-a37c-1ab16e85d955\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"a1c1cf06-c020-4304-9f7c-f9c0c28938fc\",\n            \"is_active\": true,\n            \"price\": 143.232,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362466.351,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.286224,\n            \"exchange_id\": \"868e5035-1cb9-413c-a8f8-5d08d2b97e77\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"5960d7d7-f290-4a43-aa6c-16eaca5da57d\",\n            \"is_active\": true,\n            \"price\": 142.889,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362466.848,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.28072,\n            \"exchange_id\": \"3c4ead8c-162a-4d70-ad75-a22ff8df2cdf\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"4d3c13b0-6b6b-4aa4-b293-f9871b1acdb1\",\n            \"is_active\": true,\n            \"price\": 142.545,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362467.354,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.275232,\n            \"exchange_id\": \"231ccb05-af47-4b39-a636-fe5dd8366421\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"aa27b90c-cbbe-4a2a-a916-8e4302df21f4\",\n            \"is_active\": true,\n            \"price\": 142.202,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362467.873,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.269744,\n            \"exchange_id\": \"99d4e79d-9dd5-49bd-ab2d-96e577e328e2\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"5a1289dd-574b-445d-9d66-64761532be12\",\n            \"is_active\": true,\n            \"price\": 141.859,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362468.366,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.264256,\n            \"exchange_id\": \"47751ce1-6a9b-4952-b334-5a08a791752c\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"351587f1-cc99-43e0-9cf3-36d05ccb7ddc\",\n            \"is_active\": true,\n            \"price\": 141.516,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362468.856,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.258768,\n            \"exchange_id\": \"00d8a607-ac35-4f09-a650-d8edbb7f9881\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"a6f3b844-69bc-4fdc-beb7-26485fc055b8\",\n            \"is_active\": true,\n            \"price\": 141.173,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362469.344,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.25328,\n            \"exchange_id\": \"9f0b79e3-8296-48d5-ad94-8ce4ddbce87e\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"8c8437f0-b714-4963-8ed6-00017f83389c\",\n            \"is_active\": true,\n            \"price\": 140.83,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362469.84,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.247792,\n            \"exchange_id\": \"2e028d69-8a07-4ba6-9da9-ef8b343a1d51\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"4be47863-7c6a-4c87-8cd6-d8a20cb17870\",\n            \"is_active\": true,\n            \"price\": 140.487,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362470.333,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        },\n        {\n          \"origin_value\": {\n            \"amount\": 0.016,\n            \"broker_applied\": false,\n            \"cost\": 2.242304,\n            \"exchange_id\": \"cfc91ba9-6440-4c4c-addf-a2973425e4cf\",\n            \"fee\": null,\n            \"filled\": 0,\n            \"id\": \"bfc8bb3a-72e7-4572-9ebc-ffb43067a17b\",\n            \"is_active\": true,\n            \"price\": 140.144,\n            \"quantity_currency\": \"SOL\",\n            \"reduceOnly\": false,\n            \"self-managed\": false,\n            \"side\": \"buy\",\n            \"status\": \"open\",\n            \"symbol\": \"SOL/USDT\",\n            \"tag\": null,\n            \"takerOrMaker\": \"maker\",\n            \"timestamp\": 1751362470.852,\n            \"triggerAbove\": false,\n            \"type\": \"limit\"\n          }\n        }\n      ]\n\"\"\""
  },
  {
    "path": "Trading/Mode/grid_trading_mode/tests/test_grid_trading_mode.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport contextlib\nimport numpy\nimport pytest\nimport os.path\nimport asyncio\nimport decimal\nimport copy\nimport mock\nimport time\n\nimport async_channel.util as channel_util\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_backtesting.api as backtesting_api\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.tests.test_config as test_config\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.signals as trading_signals\nimport octobot_trading.modes as trading_modes\nimport tentacles.Trading.Mode.grid_trading_mode.grid_trading as grid_trading\nimport tentacles.Trading.Mode.grid_trading_mode.tests.open_orders_data as open_orders_data\nimport tentacles.Trading.Mode.staggered_orders_trading_mode.staggered_orders_trading as staggered_orders_trading\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.memory_check_util as memory_check_util\nimport tests.test_utils.test_exchanges as test_exchanges\nimport tests.test_utils.trading_modes as test_trading_modes\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def _init_trading_mode(config, exchange_manager, symbol):\n    staggered_orders_trading.StaggeredOrdersTradingModeProducer.SCHEDULE_ORDERS_CREATION_ON_START = False\n    mode = grid_trading.GridTradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    # mode.trading_config = _get_multi_symbol_staggered_config()\n    await mode.initialize()\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n    mode.producers[0].PRICE_FETCHING_TIMEOUT = 0.5\n    mode.producers[0].allow_order_funds_redispatch = True\n    return mode, mode.producers[0]\n\n\n@contextlib.asynccontextmanager\nasync def _get_tools(symbol, btc_holdings=None, additional_portfolio={}, fees=None):\n    exchange_manager = None\n    try:\n        tentacles_manager_api.reload_tentacle_info()\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 1000\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"BTC\"] = 10 if btc_holdings is None else btc_holdings\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO].update(additional_portfolio)\n        if fees is not None:\n            config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_SIMULATOR_FEES][\n                commons_constants.CONFIG_SIMULATOR_FEES_TAKER] = fees\n            config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_SIMULATOR_FEES][\n                commons_constants.CONFIG_SIMULATOR_FEES_MAKER] = fees\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        # set BTC/USDT price at 1000 USDT\n        trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n        mode, producer = await _init_trading_mode(config, exchange_manager, symbol)\n\n        producer.flat_spread = decimal.Decimal(10)\n        producer.flat_increment = decimal.Decimal(5)\n        producer.buy_orders_count = 25\n        producer.sell_orders_count = 25\n        producer.compensate_for_missed_mirror_order = True\n        test_trading_modes.set_ready_to_start(producer)\n\n        yield producer, mode.get_trading_mode_consumers()[0], exchange_manager\n    finally:\n        if exchange_manager:\n            await _stop(exchange_manager)\n\n\nasync def _stop(exchange_manager):\n    if exchange_manager is None:\n        return\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n\n\nasync def test_run_independent_backtestings_with_memory_check():\n    \"\"\"\n    Should always be called first here to avoid other tests' related memory check issues\n    \"\"\"\n    staggered_orders_trading.StaggeredOrdersTradingModeProducer.SCHEDULE_ORDERS_CREATION_ON_START = True\n    tentacles_setup_config = tentacles_manager_api.create_tentacles_setup_config_with_tentacles(\n        grid_trading.GridTradingMode\n    )\n    await memory_check_util.run_independent_backtestings_with_memory_check(test_config.load_test_config(),\n                                                                           tentacles_setup_config)\n\n\nasync def test_init_allowed_price_ranges_with_flat_values():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        producer.sell_price_range = grid_trading.AllowedPriceRange()\n        producer.buy_price_range = grid_trading.AllowedPriceRange()\n        producer.flat_spread = decimal.Decimal(12)\n        producer.flat_increment = decimal.Decimal(5)\n        producer.sell_orders_count = 20\n        producer.buy_orders_count = 5\n        producer._init_allowed_price_ranges(100)\n        # price + half spread + increment for each order to create after 1st one\n        assert producer.sell_price_range.higher_bound == 100 + 12/2 + 5*(20-1)\n        assert producer.sell_price_range.lower_bound == 100 + 12/2\n        assert producer.buy_price_range.higher_bound == 100 - 12/2\n        # price - half spread - increment for each order to create after 1st one\n        assert producer.buy_price_range.lower_bound == 100 - 12/2 - 5*(5-1)\n\n\nasync def test_init_allowed_price_ranges_with_percent_values():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        producer.sell_price_range = grid_trading.AllowedPriceRange()\n        producer.buy_price_range = grid_trading.AllowedPriceRange()\n        # used with default configuration\n        producer.spread = decimal.Decimal(\"0.05\")   # 5%\n        producer.increment = decimal.Decimal(\"0.02\")   # 2%\n        producer.flat_spread = None\n        producer.flat_increment = None\n        producer.sell_orders_count = 20\n        producer.buy_orders_count = 5\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer._init_allowed_price_ranges(100)\n        # price + half spread + increment for each order to create after 1st one\n        assert producer.flat_spread == 5\n        assert producer.flat_increment == 2\n        assert producer.sell_price_range.higher_bound == decimal.Decimal(str(100 + 5/2 + 2*(20-1)))\n        assert producer.sell_price_range.lower_bound == decimal.Decimal(str(100 + 5/2))\n        assert producer.buy_price_range.higher_bound == decimal.Decimal(str(100 - 5/2))\n        # price - half spread - increment for each order to create after 1st one\n        assert producer.buy_price_range.lower_bound == decimal.Decimal(str(100 - 5/2 - 2*(5-1)))\n\n\nasync def test_create_orders_with_default_config():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        producer.spread = producer.increment = producer.flat_spread = producer.flat_increment = \\\n            producer.buy_orders_count = producer.sell_orders_count = None\n        producer.trading_mode.trading_config[producer.trading_mode.CONFIG_PAIR_SETTINGS] = []\n\n        assert producer._load_symbol_trading_config() is True\n        producer.read_config()\n\n        assert producer.spread is not None\n        assert producer.increment is not None\n        assert producer.flat_spread is None\n        assert producer.flat_increment is None\n        assert producer.buy_orders_count is not None\n        assert producer.sell_orders_count is not None\n\n        producer.sell_funds = decimal.Decimal(\"0.00006\")  # 5 orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 24 orders\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        await producer._ensure_staggered_orders()\n        # create orders as with normal config (except that it's the default one)\n        btc_available_funds = producer._get_available_funds(\"BTC\")\n        usd_available_funds = producer._get_available_funds(\"USDT\")\n\n        used_btc = 10 - btc_available_funds\n        used_usd = 1000 - usd_available_funds\n\n        assert producer.buy_funds * decimal.Decimal(0.95) <= used_usd <= producer.buy_funds\n        assert producer.sell_funds * decimal.Decimal(0.95) <= used_btc <= producer.sell_funds\n\n        # btc_available_funds for reduced because orders are not created\n        assert 10 - 0.001 <= btc_available_funds < 10\n        assert 1000 - 100 <= usd_available_funds < 1000\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 5 + producer.buy_orders_count))\n        created_orders = trading_api.get_open_orders(exchange_manager)\n        created_buy_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.BUY]\n        created_sell_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.SELL]\n        assert len(created_buy_orders) == producer.buy_orders_count == 20\n        assert len(created_sell_orders) < producer.sell_orders_count\n        assert len(created_sell_orders) == 5\n        # ensure only orders closest to the current price have been created\n        min_buy_price = 4000 - (producer.flat_spread / 2) - (producer.flat_increment * (len(created_buy_orders) - 1))\n        assert all(\n            o.origin_price >= min_buy_price for o in created_buy_orders\n        )\n        max_sell_price = 4000 + (producer.flat_spread / 2) + (producer.flat_increment * (len(created_sell_orders) - 1))\n        assert all(\n            o.origin_price <= max_sell_price for o in created_sell_orders\n        )\n        pf_btc_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pf_usd_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pf_btc_available_funds >= 10 - 0.00006\n        assert pf_usd_available_funds >= 1000 - 1\n\n        assert pf_btc_available_funds >= btc_available_funds\n        assert pf_usd_available_funds >= usd_available_funds\n\n\nasync def test_create_orders_without_enough_funds_for_all_orders_16_total_orders():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n\n        producer.sell_funds = decimal.Decimal(\"0.00006\")  # 5 orders\n        producer.buy_funds = decimal.Decimal(\"0.5\")  # 11 orders\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        await producer._ensure_staggered_orders()\n        btc_available_funds = producer._get_available_funds(\"BTC\")\n        usd_available_funds = producer._get_available_funds(\"USDT\")\n\n        used_btc = 10 - btc_available_funds\n        used_usd = 1000 - usd_available_funds\n\n        assert used_usd >= producer.buy_funds * decimal.Decimal(0.99)\n        assert used_btc >= producer.sell_funds * decimal.Decimal(0.99)\n\n        # btc_available_funds for reduced because orders are not created\n        assert 10 - 0.001 <= btc_available_funds < 10\n        assert 1000 - 100 <= usd_available_funds < 1000\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 5 + 11))\n        created_orders = trading_api.get_open_orders(exchange_manager)\n        created_buy_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.BUY]\n        created_sell_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.SELL]\n        assert len(created_buy_orders) < producer.buy_orders_count\n        assert len(created_buy_orders) == 11\n        assert len(created_sell_orders) < producer.sell_orders_count\n        assert len(created_sell_orders) == 5\n        # ensure only orders closest to the current price have been created\n        min_buy_price = 4000 - (producer.flat_spread / 2) - (producer.flat_increment * (len(created_buy_orders) - 1))\n        assert all(\n            o.origin_price >= min_buy_price for o in created_buy_orders\n        )\n        max_sell_price = 4000 + (producer.flat_spread / 2) + (producer.flat_increment * (len(created_sell_orders) - 1))\n        assert all(\n            o.origin_price <= max_sell_price for o in created_sell_orders\n        )\n        pf_btc_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pf_usd_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pf_btc_available_funds >= 10 - 0.00006\n        assert pf_usd_available_funds >= 1000 - 0.5\n\n        assert pf_btc_available_funds >= btc_available_funds\n        assert pf_usd_available_funds >= usd_available_funds\n\n\nasync def test_create_orders_without_enough_funds_for_all_orders_3_total_orders():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n\n        producer.buy_funds = decimal.Decimal(\"0.07\")  # 1 order\n        producer.sell_funds = decimal.Decimal(\"0.000025\")  # 2 orders\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        await producer._ensure_staggered_orders()\n        btc_available_funds = producer._get_available_funds(\"BTC\")\n        usd_available_funds = producer._get_available_funds(\"USDT\")\n\n        used_btc = 10 - btc_available_funds\n        used_usd = 1000 - usd_available_funds\n\n        assert used_usd >= producer.buy_funds * decimal.Decimal(0.99)\n        assert used_btc >= producer.sell_funds * decimal.Decimal(0.99)\n\n        # btc_available_funds for reduced because orders are not created\n        assert 10 - 0.001 <= btc_available_funds < 10\n        assert 1000 - 100 <= usd_available_funds < 1000\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 1 + 2))\n        created_orders = trading_api.get_open_orders(exchange_manager)\n        created_buy_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.BUY]\n        created_sell_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.SELL]\n        assert len(created_buy_orders) < producer.buy_orders_count\n        assert len(created_buy_orders) == 1\n        assert len(created_sell_orders) < producer.sell_orders_count\n        assert len(created_sell_orders) == 2\n        # ensure only orders closest to the current price have been created\n        min_buy_price = 4000 - (producer.flat_spread / 2) - (producer.flat_increment * (len(created_buy_orders) - 1))\n        assert all(\n            o.origin_price >= min_buy_price for o in created_buy_orders\n        )\n        max_sell_price = 4000 + (producer.flat_spread / 2) + (producer.flat_increment * (len(created_sell_orders) - 1))\n        assert all(\n            o.origin_price <= max_sell_price for o in created_sell_orders\n        )\n        pf_btc_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pf_usd_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pf_btc_available_funds >= 10 - 0.000025\n        assert pf_usd_available_funds >= 1000 - 0.07\n\n        assert pf_btc_available_funds >= btc_available_funds\n        assert pf_usd_available_funds >= usd_available_funds\n\n\nasync def test_create_orders_with_fixed_volume_per_order():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n\n        producer.buy_volume_per_order = decimal.Decimal(\"0.1\")\n        producer.sell_volume_per_order = decimal.Decimal(\"0.3\")\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 27))\n        created_orders = trading_api.get_open_orders(exchange_manager)\n        created_buy_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.BUY]\n        created_sell_orders = [o for o in created_orders if o.side is trading_enums.TradeOrderSide.SELL]\n        assert len(created_buy_orders) == 2  # not enough funds to create more orders\n        assert len(created_sell_orders) == producer.sell_orders_count  # 25\n\n        # ensure only closest orders got created with the right value and in the right order\n        assert created_buy_orders[0].origin_price == 3995\n        assert created_buy_orders[1].origin_price == 3990\n        assert created_sell_orders[0].origin_price == 4005\n        assert created_sell_orders[1].origin_price == 4010\n        assert created_sell_orders[0] is created_orders[0]\n        assert all(o.origin_quantity == producer.buy_volume_per_order for o in created_buy_orders)\n        assert all(o.origin_quantity == producer.sell_volume_per_order for o in created_sell_orders)\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 4000)\n\n\nasync def test_start_with_existing_valid_orders():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        orders_count = 20 + 24\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders orders (price is negative for the last 6 orders)\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n\n        # new evaluation, same price\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        # did nothing\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert original_orders[0] is trading_api.get_open_orders(exchange_manager)[0]\n        assert original_orders[-1] is trading_api.get_open_orders(exchange_manager)[-1]\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count\n        first_buy_index = 25\n\n        # new evaluation, price changed\n        # order would be filled\n        to_fill_order = original_orders[first_buy_index]\n        price = 95\n        assert price == to_fill_order.origin_price\n        await _fill_order(to_fill_order, exchange_manager, price, producer=producer)\n        await asyncio.create_task(_wait_for_orders_creation(2))\n        # did nothing: orders got replaced\n        assert len(original_orders) == len(trading_api.get_open_orders(exchange_manager))\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        # did nothing\n        assert len(original_orders) == len(trading_api.get_open_orders(exchange_manager))\n\n        # orders gets cancelled\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        to_cancel = [open_orders[20], open_orders[18], open_orders[3]]\n        for order in to_cancel:\n            await exchange_manager.trader.cancel_order(order)\n        post_available = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(to_cancel)\n\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(orders_count))\n        # restored orders\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_filled_orders_without_recent_trades():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"10000\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation 1: orders get filled but not replaced => price got up to 110 and down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if 90 <= o.origin_price <= 110]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        # clear trades\n        await trading_api.clear_trades_storage_history(exchange_manager)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        price = 96\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_portfolio\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n        # offline simulation 2: orders get filled but not replaced => price got down to 50\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        price = 50\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if price <= o.origin_price <= 100]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        # clear trades\n        await trading_api.clear_trades_storage_history(exchange_manager)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_portfolio\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_filled_orders_with_recent_trades():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"10000\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if 90 <= o.origin_price <= 110]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        price = 95\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_portfolio\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n        # offline simulation 2: orders get filled but not replaced => price got down to 50\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        price = 50\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if price <= o.origin_price <= 100]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_portfolio\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_filled_orders_close_to_price_with_recent_trades_considering_fees():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"10000\")  # 25 buy orders\n        producer.flat_spread = decimal.Decimal(\"200\")\n        producer.flat_increment = decimal.Decimal(\"75\")\n        producer.ignore_exchange_fees = False\n        orders_count = 25 + 25\n\n        initial_price = 29247.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29147.16')]\n        assert len(offline_filled_orders) == 1\n        offline_filled = offline_filled_orders[0]\n        await _fill_order(offline_filled, exchange_manager, trigger_update_callback=False, producer=producer)\n        # offline_filled is a buy order: now have mode BTC\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - 1\n\n        # back online: restore orders according to current price => create sell missing order\n        price = 29127.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled_orders)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available < post_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        _check_created_orders(producer, open_orders, initial_price)\n        new_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29272.16')]\n        assert len(new_orders) == 1\n        new_order = new_orders[0]\n        assert new_order.side is trading_enums.TradeOrderSide.SELL\n        # offline_filled - fees\n        trade = trading_api.get_trade_history(exchange_manager)[0]\n        fees = trade.fee[trading_enums.FeePropertyColumns.COST.value]\n        symbol_market = exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n        assert new_order.origin_quantity == \\\n               trading_personal_data.decimal_adapt_quantity(symbol_market, offline_filled.origin_quantity - fees)\n\n\nasync def test_start_after_offline_filled_orders_close_to_price_with_recent_trades_ignoring_fees_with_enough_available_funds():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"10000\")  # 25 buy orders\n        producer.flat_spread = decimal.Decimal(\"200\")\n        producer.flat_increment = decimal.Decimal(\"75\")\n        producer.ignore_exchange_fees = True\n        orders_count = 25 + 25\n\n        initial_price = 29247.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29147.16')]\n        assert len(offline_filled_orders) == 1\n        offline_filled = offline_filled_orders[0]\n        await _fill_order(offline_filled, exchange_manager, trigger_update_callback=False, producer=producer)\n        # offline_filled is a buy order: now have mode BTC\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - 1\n\n        # back online: restore orders according to current price => create sell missing order\n        price = 29127.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled_orders)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available < post_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        _check_created_orders(producer, open_orders, initial_price)\n        new_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29272.16')]\n        assert len(new_orders) == 1\n        new_order = new_orders[0]\n        assert new_order.side is trading_enums.TradeOrderSide.SELL\n        # offline_filled - fees\n        trade = trading_api.get_trade_history(exchange_manager)[0]\n        fees = trade.fee[trading_enums.FeePropertyColumns.COST.value]\n        assert fees > trading_constants.ZERO\n        assert new_order.origin_quantity == offline_filled.origin_quantity  # trading fees exist but are not taken into account\n\n\nasync def test_start_after_offline_filled_orders_close_to_price_with_recent_trades_ignoring_fees_without_enough_available_sell_funds():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"10000\")  # 25 buy orders\n        producer.flat_spread = decimal.Decimal(\"200\")\n        producer.flat_increment = decimal.Decimal(\"75\")\n        producer.ignore_exchange_fees = True\n        orders_count = 25 + 25\n\n        initial_price = 29247.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29147.16')]\n        assert len(offline_filled_orders) == 1\n        offline_filled = offline_filled_orders[0]\n        assert offline_filled.side is trading_enums.TradeOrderSide.BUY\n        await _fill_order(offline_filled, exchange_manager, trigger_update_callback=False, producer=producer)\n        # offline_filled is a buy order: now have mode BTC\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - 1\n        assert offline_filled.origin_quantity == decimal.Decimal(\"0.00136765\")\n        trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available = decimal.Decimal(\"0.00116765111111111111111111111\") # less than order quantity to simulate fees\n\n        # back online: restore orders according to current price => create missing sell order\n        price = 29127.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled_orders)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available < post_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        _check_created_orders(producer, open_orders, initial_price)\n        new_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29272.16')]\n        assert len(new_orders) == 1\n        new_order = new_orders[0]\n        assert new_order.side is trading_enums.TradeOrderSide.SELL\n        # offline_filled - fees\n        trade = trading_api.get_trade_history(exchange_manager)[0]\n        fees = trade.fee[trading_enums.FeePropertyColumns.COST.value]\n        assert fees > trading_constants.ZERO\n        assert new_order.origin_quantity < offline_filled.origin_quantity  # adapted amount to available funds\n        assert new_order.origin_quantity == decimal.Decimal(\"0.00116765\")\n\n\nasync def test_start_after_offline_filled_orders_close_to_price_with_recent_trades_ignoring_fees_without_enough_available_buy_funds():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"10000\")  # 25 buy orders\n        producer.flat_spread = decimal.Decimal(\"200\")\n        producer.flat_increment = decimal.Decimal(\"75\")\n        producer.ignore_exchange_fees = True\n        orders_count = 25 + 25\n\n        initial_price = 29247.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29347.16')]\n        assert len(offline_filled_orders) == 1\n        offline_filled = offline_filled_orders[0]\n        assert offline_filled.side is trading_enums.TradeOrderSide.SELL\n        await _fill_order(offline_filled, exchange_manager, trigger_update_callback=False, producer=producer)\n        offline_filled_cost = offline_filled.total_cost\n        assert offline_filled_cost == decimal.Decimal(\"1173.8864\")\n        # offline_filled is a buy order: now have mode BTC\n        USDT_assets = trading_api.get_portfolio_currency(exchange_manager, \"USDT\")\n        USDT_assets.available = decimal.Decimal(\"666\") # less than order quantity to simulate fees\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - 1\n        assert offline_filled.origin_quantity == decimal.Decimal(\"0.04\")\n\n        # back online: restore orders according to current price => create missing buy order\n        price = 29227.16\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled_orders)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available < post_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        _check_created_orders(producer, open_orders, initial_price)\n        new_orders = [o for o in open_orders if o.origin_price == decimal.Decimal('29222.16')]\n        assert len(new_orders) == 1\n        new_order = new_orders[0]\n        assert new_order.side is trading_enums.TradeOrderSide.BUY\n        # offline_filled - fees\n        trade = trading_api.get_trade_history(exchange_manager)[0]\n        fees = trade.fee[trading_enums.FeePropertyColumns.COST.value]\n        assert fees > trading_constants.ZERO\n        assert new_order.origin_quantity < offline_filled.origin_quantity  # adapted amount to available funds\n        assert new_order.origin_quantity == decimal.Decimal(\"0.02210719\")\n        assert new_order.total_cost == decimal.Decimal(\"646.0198433304\")    # < 666\n\n\nasync def test_start_after_offline_full_sell_side_filled_orders_with_recent_trades():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        price = max(order.origin_price for order in offline_filled) * 2\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        assert producer.operational_depth > orders_count\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_portfolio\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        assert all(\n            order.side == trading_enums.TradeOrderSide.BUY\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_full_sell_side_filled_orders_price_back():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to average origin sell orders\n        price = offline_filled[len(offline_filled)//2].origin_price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n\n        def _get_fees_for_currency(fee, currency):\n            if currency == \"USDT\":\n                return decimal.Decimal(\"0.022\")\n            return trading_constants.ZERO\n\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with _assert_adapt_order_quantity_because_fees(_get_fees_for_currency) \\\n                as adapt_order_quantity_because_fees_mock:\n                await producer._ensure_staggered_orders()\n                adapt_order_quantity_because_fees_mock.assert_called_once_with(\n                    producer.exchange_manager, producer.trading_mode.symbol, trading_enums.TraderOrderType.BUY_MARKET,\n                    decimal.Decimal('0.25714721'),\n                    decimal.Decimal('165'),\n                    trading_enums.TradeOrderSide.BUY,\n                )\n        # restored orders (and create up to 50 orders as all orders can be created)\n        assert producer.operational_depth > orders_count\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_portfolio\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        assert not all(\n            order.side == trading_enums.TradeOrderSide.BUY\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_full_buy_side_filled_orders_price_back_with_recent_trades():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to average origin buy orders\n        price = offline_filled[len(offline_filled)//2].origin_price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        assert not all(\n            order.side == trading_enums.TradeOrderSide.BUY\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_buy_side_10_filled():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY][:10]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to average origin buy orders\n        price = offline_filled[len(offline_filled)//2].origin_price + 1\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with _assert_adapt_order_quantity_because_fees(None) \\\n                as adapt_order_quantity_because_fees_mock:\n                await producer._ensure_staggered_orders()\n                adapt_order_quantity_because_fees_mock.assert_called_once_with(\n                    producer.exchange_manager, producer.trading_mode.symbol, trading_enums.TraderOrderType.SELL_MARKET,\n                    decimal.Decimal('0.00320847831'),\n                    decimal.Decimal('71'),\n                    trading_enums.TradeOrderSide.SELL,\n                )\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # created 5 more sell orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 + 5\n        # restored 5 of the 10 filled buy orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 19 - 5\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n\nasync def test_start_after_offline_x_filled_and_price_back_should_sell_to_recreate_buy():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        orders_count = 25 + 25\n\n        price = decimal.Decimal(200)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price moved to 150\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.BUY and o.origin_price >= decimal.Decimal(\"150\")\n        ]\n        # this is 10 orders\n        assert len(offline_filled) == 10\n        max_filled_order_price = max(o.origin_price for o in offline_filled)\n        assert max_filled_order_price == decimal.Decimal(195)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # buy orders filled: BTC increased\n        assert pre_btc_portfolio < post_btc_portfolio\n        # no sell order filled, available USDT is constant\n        assert pre_usdt_portfolio == post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to 180: should quickly sell BTC bought between 150 and 180 to be able to\n        # create buy orders between 150 and 180\n        price = decimal.Decimal(180)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_btc_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # created 4 additional sell orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 + 4\n        # restored 6 out of 10 filled buy orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 25 - 10 + 6\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n\n\nasync def test_start_after_offline_1_filled_and_price_back_should_NOT_sell_to_recreate_buy_but_just_create_a_sell_order():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        orders_count = 25 + 25\n\n        price = decimal.Decimal(200)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: 1 buy order get filled but not replaced => price moved to 194 (first buy order is at 195)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.BUY and o.origin_price >= decimal.Decimal(\"194\")\n        ]\n        assert len(offline_filled) == 1\n        assert offline_filled[0].origin_price == decimal.Decimal(195)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # buy orders filled: BTC increased\n        assert pre_btc_portfolio < post_btc_portfolio\n        # no sell order filled, available USDT is constant\n        assert pre_usdt_portfolio == post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to 198: do not market sell BTC but create a new sell order instead \n        price = decimal.Decimal(198)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        # lower sell order is at 205\n        assert min(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL) == decimal.Decimal(205)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with mock.patch.object(producer, \"_pack_and_balance_missing_orders\", mock.AsyncMock()) as _pack_and_balance_missing_orders_mock:\n                await producer._ensure_staggered_orders()\n                # does not create missing mirror orders market orders\n                _pack_and_balance_missing_orders_mock.assert_not_called()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_btc_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # created 1 additional sell order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 + 1\n        # created a new sell order at 200\n        assert min(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL) == decimal.Decimal(200)\n        # no created buy order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 25 - 1\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n\n\nasync def test_start_after_offline_1_filled_and_price_back_should_NOT_sell_to_recreate_buy_but_just_create_a_sell_order_with_surrounding_partially_filled_orders():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        orders_count = 25 + 25\n\n        price = decimal.Decimal(200)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: 1 buy order get filled but not replaced => price moved to 194 (first buy order is at 195)\n        # and 2nd buy order get partially filled\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.BUY and o.origin_price >= decimal.Decimal(\"194\")\n        ]\n        assert len(offline_filled) == 1\n        assert offline_filled[0].origin_price == decimal.Decimal(195)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # buy orders filled: BTC increased\n        assert pre_btc_portfolio < post_btc_portfolio\n        # no sell order filled, available USDT is constant\n        assert pre_usdt_portfolio == post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        partially_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.BUY and o.origin_price == decimal.Decimal(\"190\") or o.origin_price == decimal.Decimal(\"205\")\n        ]\n        assert len(partially_filled) == 2\n        for partially_filled_order in partially_filled:\n            partially_filled_order.filled_quantity = partially_filled_order.origin_quantity / decimal.Decimal(2)\n            partially_filled_order.filled_price = partially_filled_order.origin_price\n            # add trade corresponding to the partial order fill\n            assert await exchange_manager.exchange_personal_data.handle_trade_instance_update(\n                exchange_manager.trader.convert_order_to_trade(partially_filled_order)\n            ) is True\n            trade = exchange_manager.exchange_personal_data.trades_manager.get_trade_from_order_id(partially_filled_order.order_id)\n            assert trade.executed_quantity == partially_filled_order.filled_quantity\n            assert trade.executed_price == partially_filled_order.origin_price\n            trade.executed_time = time.time()  # these trades are the most recent ones\n\n        # back online: restore orders according to current price\n        # simulate current price as back to 198: do not market sell BTC but create a new sell order instead \n        price = decimal.Decimal(198)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        # lower sell order is at 205\n        assert min(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL) == decimal.Decimal(205)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with mock.patch.object(producer, \"_pack_and_balance_missing_orders\", mock.AsyncMock()) as _pack_and_balance_missing_orders_mock:\n                await producer._ensure_staggered_orders()\n                # does not create missing mirror orders market orders\n                _pack_and_balance_missing_orders_mock.assert_not_called()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available <= post_btc_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # created 1 additional sell order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 + 1\n        # created a new sell order at 200\n        assert min(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL) == decimal.Decimal(200)\n        # no created buy order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 25 - 1\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n\n\nasync def test_start_after_offline_1_filled_and_price_back_should_NOT_buy_to_recreate_sell_but_just_create_a_buy_order():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        orders_count = 25 + 25\n\n        price = decimal.Decimal(200)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: 1 sell order get filled but not replaced => price moved to 206 (first sell order is at 205)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.SELL and o.origin_price <= decimal.Decimal(\"206\")\n        ]\n        assert len(offline_filled) == 1\n        assert offline_filled[0].origin_price == decimal.Decimal(205)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # sell orders filled: BTC is constant\n        assert pre_btc_portfolio == post_btc_portfolio\n        # no sell order filled, USDT increased\n        assert pre_usdt_portfolio < post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to 202: do not market sell BTC but create a new buy order instead \n        price = decimal.Decimal(202)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        # higest buy order is at 195\n        assert max(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY) == decimal.Decimal(195)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with mock.patch.object(producer, \"_pack_and_balance_missing_orders\", mock.AsyncMock()) as _pack_and_balance_missing_orders_mock:\n                await producer._ensure_staggered_orders()\n                # does not create missing mirror orders market orders\n                _pack_and_balance_missing_orders_mock.assert_not_called()\n\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_usdt_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # no created sell order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 - 1\n        assert min(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL) == decimal.Decimal(210)\n        # created a new buy order at 200\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 25 + 1\n        assert max(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY) == decimal.Decimal(200)\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n\n\nasync def test_start_after_offline_2_filled_and_price_back_should_buy_to_recreate_sell():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        orders_count = 25 + 25\n\n        price = decimal.Decimal(200)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: 2 sell orders get filled but not replaced => price moved to 211 (first sell order is at 211)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.SELL and o.origin_price <= decimal.Decimal(\"211\")\n        ]\n        assert len(offline_filled) == 2\n        assert offline_filled[0].origin_price == decimal.Decimal(205)\n        assert offline_filled[1].origin_price == decimal.Decimal(210)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # sell orders filled: BTC is constant\n        assert pre_btc_portfolio == post_btc_portfolio\n        # no sell order filled, USDT increased\n        assert pre_usdt_portfolio < post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to 202: do not market sell BTC but create a new buy order instead \n        price = decimal.Decimal(202)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        # higest buy order is at 195\n        assert max(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY) == decimal.Decimal(195)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with mock.patch.object(producer, \"_pack_and_balance_missing_orders\", mock.AsyncMock(wraps=producer._pack_and_balance_missing_orders)) as _pack_and_balance_missing_orders_mock:\n                await producer._ensure_staggered_orders()\n                # DOES create a missing mirror orders market order to compensate for the missing sell order\n                _pack_and_balance_missing_orders_mock.assert_called_once()\n\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_usdt_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # recreated 1 sell order at 210\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 - 1\n        assert min(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL) == decimal.Decimal(210)\n        # created a new buy order at 200\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 25 + 1\n        assert max(order.origin_price for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY) == decimal.Decimal(200)\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n\n\nasync def test_start_after_offline_x_filled_and_price_back_should_buy_to_recreate_sell():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        orders_count = 25 + 25\n\n        price = decimal.Decimal(200)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price moved to 150\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.SELL and o.origin_price <= decimal.Decimal(\"250\")\n        ]\n        # this is 10 orders\n        assert len(offline_filled) == 10\n        max_filled_order_price = max(o.origin_price for o in offline_filled)\n        assert max_filled_order_price == decimal.Decimal(250)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # buy orders filled: available BTC is constant\n        assert pre_btc_portfolio == post_btc_portfolio\n        # no sell order filled, available USDT increased\n        assert pre_usdt_portfolio <= post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as back to 220: should quickly buy BTC sold between 250 and 220 to be able to\n        # create sell orders between 220 and 250\n        price = decimal.Decimal(220)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_usdt_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # restored 6 out of 10 sell orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 25 - 10 + 6\n        # created 4 additional buy orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 25 + 4\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n\n\nasync def test_start_after_offline_x_filled_and_missing_should_recreate_1_sell():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # forced config\n        producer.buy_funds = producer.sell_funds = 0\n        producer.allow_order_funds_redispatch = True\n        producer.buy_orders_count = producer.sell_orders_count = 5\n        producer.compensate_for_missed_mirror_order = True\n        producer.enable_trailing_down = False\n        producer.enable_trailing_up = True\n        producer.flat_increment = decimal.Decimal(100)\n        producer.flat_spread = decimal.Decimal(300)\n        producer.reinvest_profits = False\n        producer.sell_volume_per_order = producer.buy_volume_per_order = False\n        producer.starting_price = 0\n        producer.use_existing_orders_only = False\n\n        orders_count = producer.buy_orders_count + producer.sell_orders_count\n\n\n        initial_price = decimal.Decimal(\"105278.1\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        btc_pf = trading_api.get_portfolio_currency(exchange_manager, \"BTC\")\n        usdt_pf = trading_api.get_portfolio_currency(exchange_manager, \"USDT\")\n        btc_pf.available = decimal.Decimal(\"0.00141858\")\n        btc_pf.total = decimal.Decimal(\"0.00141858\")\n        usdt_pf.available = decimal.Decimal(\"150.505098\")\n        usdt_pf.total = decimal.Decimal(\"150.505098\")\n\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        assert sorted([\n            order.origin_price for order in original_orders\n        ]) == [\n            # buy orders\n            decimal.Decimal('104728.1'), decimal.Decimal('104828.1'), decimal.Decimal('104928.1'),\n            decimal.Decimal('105028.1'), decimal.Decimal('105128.1'),\n            # sell orders\n            decimal.Decimal('105428.1'), decimal.Decimal('105528.1'), decimal.Decimal('105628.1'),\n            decimal.Decimal('105728.1'), decimal.Decimal('105828.1')\n        ]\n\n        # price goes down to 105120, 105128.1 order gets filled\n        price = decimal.Decimal(\"105120\")\n        # offline simulation: price goes down to 105120, 105128.1 order gets filled\n        offline_filled = [order for order in original_orders if order.origin_price == decimal.Decimal('105128.1')]\n        assert len(offline_filled) == 1\n        assert offline_filled[0].side == trading_enums.TradeOrderSide.BUY\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        assert btc_pf.available == decimal.Decimal('0.00028861409')\n        assert btc_pf.total == decimal.Decimal('0.00170420409')\n        assert usdt_pf.available == decimal.Decimal('0.247225519')\n        assert usdt_pf.total == decimal.Decimal('120.447922929')\n\n        # back online: restore orders according to current price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # there is now 6 sell orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 6\n        # there is now 4 buy orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 4\n        # quantity is preserved\n        assert all(\n            decimal.Decimal(\"0.00028\") < order.origin_quantity < decimal.Decimal(\"0.00029\")\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), initial_price)\n\n\nasync def test_start_after_offline_x_filled_and_missing_should_recreate_5_sell_orders_no_recent_trade():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # forced config\n        producer.buy_funds = producer.sell_funds = 0\n        producer.allow_order_funds_redispatch = True\n        producer.buy_orders_count = producer.sell_orders_count = 5\n        producer.compensate_for_missed_mirror_order = True\n        producer.enable_trailing_down = False\n        producer.enable_trailing_up = True\n        producer.flat_increment = decimal.Decimal(100)\n        producer.flat_spread = decimal.Decimal(300)\n        producer.reinvest_profits = False\n        producer.sell_volume_per_order = producer.buy_volume_per_order = False\n        producer.starting_price = 0\n        producer.use_existing_orders_only = False\n\n        orders_count = producer.buy_orders_count + producer.sell_orders_count\n\n        initial_price = decimal.Decimal(\"105278.1\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        btc_pf = trading_api.get_portfolio_currency(exchange_manager, \"BTC\")\n        usdt_pf = trading_api.get_portfolio_currency(exchange_manager, \"USDT\")\n        btc_pf.available = decimal.Decimal(\"0.00141858\")\n        btc_pf.total = decimal.Decimal(\"0.00141858\")\n        usdt_pf.available = decimal.Decimal(\"150.505098\")\n        usdt_pf.total = decimal.Decimal(\"150.505098\")\n\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        assert sorted([\n            order.origin_price for order in original_orders\n        ]) == [\n            # buy orders\n            decimal.Decimal('104728.1'), decimal.Decimal('104828.1'), decimal.Decimal('104928.1'),\n            decimal.Decimal('105028.1'), decimal.Decimal('105128.1'),\n            # sell orders\n            decimal.Decimal('105428.1'), decimal.Decimal('105528.1'), decimal.Decimal('105628.1'),\n            decimal.Decimal('105728.1'), decimal.Decimal('105828.1')\n        ]\n\n        # price goes down to 104720, all buy order get filled\n        price = decimal.Decimal(\"104720\")\n        offline_filled = [order for order in original_orders if order.origin_price <= decimal.Decimal('105128.1')]\n        assert len(offline_filled) == 5\n        assert all(o.side == trading_enums.TradeOrderSide.BUY for o in offline_filled)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        assert btc_pf.available == decimal.Decimal(\"0.00143356799\")\n        assert btc_pf.total == decimal.Decimal(\"0.00284915799\")\n        assert usdt_pf.available == decimal.Decimal(\"0.247225519\")\n        assert usdt_pf.total == decimal.Decimal(\"0.247225519\")\n\n        # clear trades\n        exchange_manager.exchange_personal_data.trades_manager.trades.clear()\n\n        # back online: restore orders according to current price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # create buy orders equivalent sell orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # there is now 10 sell orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 10\n        # quantity is preserved\n        assert all(\n            decimal.Decimal(\"0.00028\") < order.origin_quantity < decimal.Decimal(\"0.00029\")\n            for order in open_orders\n        )\n        # there is now 0 buy order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 0\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), initial_price)\n\n        assert btc_pf.available == decimal.Decimal(\"0.00001571799\")\n        assert btc_pf.total == decimal.Decimal(\"0.00284915799\")\n        assert usdt_pf.available == decimal.Decimal(\"0.247225519\")\n        assert usdt_pf.total == decimal.Decimal(\"0.247225519\")\n\n\nasync def test_start_after_offline_x_filled_and_missing_should_recreate_5_buy_orders_no_recent_trade():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # forced config\n        producer.buy_funds = producer.sell_funds = 0\n        producer.allow_order_funds_redispatch = True\n        producer.buy_orders_count = producer.sell_orders_count = 5\n        producer.compensate_for_missed_mirror_order = True\n        producer.enable_trailing_down = False\n        producer.enable_trailing_up = True\n        producer.flat_increment = decimal.Decimal(100)\n        producer.flat_spread = decimal.Decimal(300)\n        producer.reinvest_profits = False\n        producer.sell_volume_per_order = producer.buy_volume_per_order = False\n        producer.starting_price = 0\n        producer.use_existing_orders_only = False\n\n        orders_count = producer.buy_orders_count + producer.sell_orders_count\n\n        initial_price = decimal.Decimal(\"105278.1\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price)\n        btc_pf = trading_api.get_portfolio_currency(exchange_manager, \"BTC\")\n        usdt_pf = trading_api.get_portfolio_currency(exchange_manager, \"USDT\")\n        btc_pf.available = decimal.Decimal(\"0.00141858\")\n        btc_pf.total = decimal.Decimal(\"0.00141858\")\n        usdt_pf.available = decimal.Decimal(\"150.505098\")\n        usdt_pf.total = decimal.Decimal(\"150.505098\")\n\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        assert sorted([\n            order.origin_price for order in original_orders\n        ]) == [\n            # buy orders\n            decimal.Decimal('104728.1'), decimal.Decimal('104828.1'), decimal.Decimal('104928.1'),\n            decimal.Decimal('105028.1'), decimal.Decimal('105128.1'),\n            # sell orders\n            decimal.Decimal('105428.1'), decimal.Decimal('105528.1'), decimal.Decimal('105628.1'),\n            decimal.Decimal('105728.1'), decimal.Decimal('105828.1')\n        ]\n\n        # price goes up to 105838, all sell order get filled\n        price = decimal.Decimal(\"105838\")\n        offline_filled = [order for order in original_orders if order.origin_price > decimal.Decimal('105128.1')]\n        assert len(offline_filled) == 5\n        assert all(o.side == trading_enums.TradeOrderSide.SELL for o in offline_filled)\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        assert btc_pf.available == decimal.Decimal(\"0.00000299\")\n        assert btc_pf.total == decimal.Decimal(\"0.00000299\")\n        assert usdt_pf.available == decimal.Decimal(\"149.623458838921\")\n        assert usdt_pf.total == decimal.Decimal(\"299.881331319921\")\n\n        # clear trades\n        exchange_manager.exchange_personal_data.trades_manager.trades.clear()\n\n        # back online: restore orders according to current price\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        # create buy orders equivalent sell orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # there is now 0 sell order\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 0\n        # there is now 10 buy orders\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 10\n        # quantity is preserved\n        assert all(\n            decimal.Decimal(\"0.00028\") < order.origin_quantity < decimal.Decimal(\"0.00029\")\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), initial_price)\n\n\nasync def test_start_after_offline_1_filled_should_create_buy_considering_fees():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        price = decimal.Decimal(\"26616.7\")\n        producer.flat_spread = decimal.Decimal(275)\n        producer.flat_increment = decimal.Decimal(125)\n        producer.buy_orders_count = 30\n        producer.sell_orders_count = 30\n        producer.ignore_exchange_fees = False\n\n        orders_count = producer.buy_orders_count + producer.sell_orders_count\n\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price moved to 26756.2\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.SELL and o.origin_price <= decimal.Decimal(\"26756.2\")\n        ]\n        # this is 1 order\n        assert len(offline_filled) == 1\n        assert offline_filled[0].origin_price == decimal.Decimal(\"26754.2\")\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # buy orders filled: available BTC is constant\n        assert pre_btc_portfolio == post_btc_portfolio\n        # no sell order filled, available USDT increased\n        assert pre_usdt_portfolio <= post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price at 26753.8\n        price = decimal.Decimal(\"26753.8\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, 1):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_usdt_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # 1 sell order is filled\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 30 - 1\n        # 1 buy order is added\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 30 + 1\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), decimal.Decimal(\"26616.7\"))\n\n\nasync def test_start_after_offline_1_filled_should_create_buy_ignoring_fees():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        price = decimal.Decimal(\"26616.7\")\n        producer.flat_spread = decimal.Decimal(275)\n        producer.flat_increment = decimal.Decimal(125)\n        producer.buy_orders_count = 30\n        producer.sell_orders_count = 30\n        producer.ignore_exchange_fees = True\n\n        orders_count = producer.buy_orders_count + producer.sell_orders_count\n\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pre_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price moved to 26756.2\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.SELL and o.origin_price <= decimal.Decimal(\"26756.2\")\n        ]\n        # this is 1 order\n        assert len(offline_filled) == 1\n        assert offline_filled[0].origin_price == decimal.Decimal(\"26754.2\")\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_btc_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        post_usdt_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        # buy orders filled: available BTC is constant\n        assert pre_btc_portfolio == post_btc_portfolio\n        # no sell order filled, available USDT increased\n        assert pre_usdt_portfolio <= post_usdt_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price at 26753.8\n        price = decimal.Decimal(\"26753.8\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, 1):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_usdt_portfolio\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # 1 sell order is filled\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 30 - 1\n        # 1 buy order is added\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 30 + 1\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), decimal.Decimal(\"26616.7\"))\n\n\nasync def test_start_after_offline_1_filled_should_create_sell():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        price = decimal.Decimal(\"26616.7\")\n        producer.flat_spread = decimal.Decimal(275)\n        producer.flat_increment = decimal.Decimal(125)\n        producer.buy_orders_count = 30\n        producer.sell_orders_count = 30\n\n        orders_count = producer.buy_orders_count + producer.sell_orders_count\n\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        # offline simulation: orders get filled but not replaced => price moved to 26756.2\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [\n            o\n            for o in open_orders\n            if o.side == trading_enums.TradeOrderSide.BUY and o.origin_price >= decimal.Decimal(\"26459.2\")\n        ]\n        # this is 1 order\n        assert len(offline_filled) == 1\n        assert offline_filled[0].origin_price == decimal.Decimal(\"26479.2\")\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price at 26409.2\n        price = decimal.Decimal(\"26409.2\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        with _assert_missing_orders_count(producer, 1):\n            await producer._ensure_staggered_orders()\n        # restored orders\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # 1 sell order is filled\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 30 + 1\n        # 1 buy order is added\n        assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 30 - 1\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), decimal.Decimal(\"26616.7\"))\n\n\nasync def test_start_after_offline_with_added_funds_increasing_orders_count():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, consumer, exchange_manager):\n        producer.sell_funds = decimal.Decimal(\"0.00005\")  # 4 sell orders\n        producer.buy_funds = decimal.Decimal(\"0.005\")  # 4 buy orders\n\n        # first start: setup orders\n        orders_count = 4 + 4\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n\n        initial_buy_orders_average_cost = numpy.mean(\n            [o.total_cost for o in original_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        )\n        initial_sell_orders_average_cost = numpy.mean(\n            [o.total_cost for o in original_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        )\n        previous_orders = original_orders\n        # 1. offline simulation: nothing happens: orders are not replaced\n        with _assert_missing_orders_count(producer, 0):\n            await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        assert all(order.is_open() for order in previous_orders)\n\n        # 2. offline simulation: funds are added (here config changed)\n        producer.sell_funds = decimal.Decimal(\"0.0001\")  # 9 sell orders\n        # triggering orders will cancel all open orders and recreate grid orders with new funds\n        with mock.patch.object(\n            consumer, \"create_order\", mock.AsyncMock(wraps=consumer.create_order)\n        ) as create_order_mock, mock.patch.object(\n            producer.trading_mode, \"cancel_order\", mock.AsyncMock(wraps=producer.trading_mode.cancel_order)\n        ) as cancel_order_mock:\n            await producer._ensure_staggered_orders()\n            # one more buy order\n            assert cancel_order_mock.call_count == orders_count # all orders are cancelled\n            assert all(\n                call.kwargs[\"dependencies\"] is None\n                for call in cancel_order_mock.mock_calls\n            )\n            new_orders_count = orders_count + 5\n            await asyncio.create_task(_check_open_orders_count(exchange_manager, new_orders_count))\n            assert create_order_mock.call_count == new_orders_count\n            cancelled_orders_dependencies = trading_signals.get_orders_dependencies(\n                [call.args[0] for call in cancel_order_mock.mock_calls]\n            )\n            # cancel orders dependencies are forwarded as dependencies for newly created orders\n            assert all(\n                call.args[3] == cancelled_orders_dependencies\n                for call in create_order_mock.mock_calls\n            )\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == new_orders_count\n        # replaced orders\n        assert new_orders[0] is not original_orders[0]\n        assert all(order.is_cancelled() for order in original_orders)\n\n        updated_buy_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        )\n        updated_sell_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        )\n        # use approx same order size\n        assert initial_buy_orders_average_cost * decimal.Decimal(str(0.9)) < \\\n               updated_buy_orders_average_cost < \\\n               initial_buy_orders_average_cost * decimal.Decimal(str(1.1))\n        assert initial_sell_orders_average_cost * decimal.Decimal(str(0.9)) < \\\n               updated_sell_orders_average_cost < \\\n               initial_sell_orders_average_cost * decimal.Decimal(str(1.1))\n\n        # 3. offline simulation: funds are added (here config changed)\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"0.01\")  # 9 sell orders\n        # triggering orders will cancel all open orders and recreate grid orders with new funds\n        await producer._ensure_staggered_orders()\n        # one more buy order\n        new_orders_count = 34\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, new_orders_count))\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == new_orders_count\n        # replaced orders\n        assert new_orders[0] is not original_orders[0]\n        assert all(order.is_cancelled() for order in original_orders)\n\n\nasync def test_start_after_offline_with_added_funds_increasing_order_sizes():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n\n        initial_buy_orders_average_cost = numpy.mean(\n            [o.total_cost for o in original_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        )\n        initial_sell_orders_average_cost = numpy.mean(\n            [o.total_cost for o in original_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        )\n        # offline simulation: funds are added\n        def _increase_funds(asset, multiplier):\n            asset.available = asset.available + asset.total * decimal.Decimal(str(multiplier - 1))\n            asset.total = asset.total * decimal.Decimal(str(multiplier))\n            return asset\n\n        portfolio = exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n        portfolio[\"BTC\"] = _increase_funds(portfolio[\"BTC\"], 2)\n        portfolio[\"USDT\"] = _increase_funds(portfolio[\"USDT\"], 4)\n\n        # triggering orders will cancel all open orders and recreate grid orders with new funds\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == orders_count\n        # replaced orders\n        assert new_orders[0] is not original_orders[0]\n        assert all(order.is_cancelled() for order in original_orders)\n\n        updated_buy_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        )\n        updated_sell_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        )\n        assert initial_buy_orders_average_cost * decimal.Decimal(str(3.5)) < \\\n               updated_buy_orders_average_cost < \\\n               initial_buy_orders_average_cost * decimal.Decimal(str(4.5))\n        assert initial_sell_orders_average_cost * decimal.Decimal(str(1.5)) < \\\n               updated_sell_orders_average_cost < \\\n               initial_sell_orders_average_cost * decimal.Decimal(str(2.5))\n\n        # increase again (2x BTC)\n        portfolio[\"BTC\"] = _increase_funds(portfolio[\"BTC\"], 2)\n        previous_orders = new_orders\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == orders_count\n        # replaced orders\n        assert new_orders[0] is not previous_orders[0]\n        assert all(order.is_cancelled() for order in previous_orders)\n\n        updated_buy_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        )\n        updated_sell_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        )\n        assert initial_buy_orders_average_cost * decimal.Decimal(str(3.5)) < \\\n               updated_buy_orders_average_cost < \\\n               initial_buy_orders_average_cost * decimal.Decimal(str(4.5))\n        assert initial_sell_orders_average_cost * decimal.Decimal(str(1.5)) * decimal.Decimal(2) \\\n               < updated_sell_orders_average_cost < \\\n               initial_sell_orders_average_cost * decimal.Decimal(str(2.5)) * decimal.Decimal(2)\n\n        # increase again (1.1x BTC)\n        portfolio[\"BTC\"] = _increase_funds(portfolio[\"BTC\"], decimal.Decimal(\"1.1\"))\n        previous_orders = new_orders\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == orders_count\n        # did not replace orders funds increase is not significant enough\n        assert new_orders[0] is previous_orders[0]\n        assert all(order.is_open() for order in previous_orders)\n\n        # increase again (12x USDT)\n        portfolio[\"USDT\"] = _increase_funds(portfolio[\"USDT\"], decimal.Decimal(\"12\"))\n        previous_orders = new_orders\n        producer.allow_order_funds_redispatch = False\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == orders_count\n        # did not replace orders: allow_order_funds_redispatch is False\n        assert new_orders[0] is previous_orders[0]\n        assert all(order.is_open() for order in previous_orders)\n        producer.allow_order_funds_redispatch = True\n\n        # fill orders before check\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        offline_filled = [o for o in new_orders if o.side == trading_enums.TradeOrderSide.BUY][:2]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(new_orders) == orders_count\n        # replaced orders\n        assert new_orders[0] is not previous_orders[0]\n        assert all(order.is_cancelled() for order in previous_orders if order not in offline_filled)\n\n        updated_buy_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        )\n        updated_sell_orders_average_cost = numpy.mean(\n            [o.total_cost for o in new_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        )\n        assert initial_buy_orders_average_cost * decimal.Decimal(str(3.5)) * decimal.Decimal(12) < \\\n               updated_buy_orders_average_cost < \\\n               initial_buy_orders_average_cost * decimal.Decimal(str(4.5)) * decimal.Decimal(12)\n        assert initial_sell_orders_average_cost * decimal.Decimal(str(1.5)) * decimal.Decimal(2) \\\n               < updated_sell_orders_average_cost < \\\n               initial_sell_orders_average_cost * decimal.Decimal(str(2.5)) * decimal.Decimal(2)\n\n\nasync def test_start_after_offline_only_buy_orders_remaining():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as still too high\n        price = offline_filled[-1].origin_price * decimal.Decimal(\"1.5\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n\n        def _get_fees_for_currency(fee, currency):\n            if currency == \"USDT\":\n                return decimal.Decimal(\"0.022\")\n            return trading_constants.ZERO\n\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with _assert_adapt_order_quantity_because_fees(_get_fees_for_currency) \\\n                as adapt_order_quantity_because_fees_mock:\n                await producer._ensure_staggered_orders()\n                await asyncio_tools.wait_asyncio_next_cycle()\n                assert adapt_order_quantity_because_fees_mock.call_count == 25\n        # restored orders (and create up to 50 orders as all orders can be created)\n        assert producer.operational_depth > orders_count\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        # did not replace orders: replace should not happen\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert sorted(new_orders, key=lambda x: x.origin_price)[0] is sorted(open_orders, key=lambda x: x.origin_price)[0]\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available <= post_portfolio\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert all(\n            order.side == trading_enums.TradeOrderSide.BUY\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n        # trigger again\n        with _assert_missing_orders_count(producer, 50 - 25 - 19):\n            with _assert_adapt_order_quantity_because_fees(_get_fees_for_currency) \\\n                as adapt_order_quantity_because_fees_mock:\n                await producer._ensure_staggered_orders()\n                await asyncio_tools.wait_asyncio_next_cycle()\n                assert adapt_order_quantity_because_fees_mock.call_count == 50 - 25 - 19\n        # filled the grid with orders up to operational depth (50)\n        orders_count = 50\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        # did not replace orders: replace should not happen\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert sorted(new_orders, key=lambda x: x.origin_price)[0] is sorted(open_orders, key=lambda x: x.origin_price)[0]\n\n\nasync def test_start_after_offline_only_sell_orders_remaining():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n\n        # back online: restore orders according to current price\n        # simulate current price as still too high\n        price = decimal.Decimal(\"0.01\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n\n        def _get_fees_for_currency(fee, currency):\n            if currency == \"USDT\":\n                return decimal.Decimal(\"0.02\")\n            return trading_constants.ZERO\n\n        with _assert_missing_orders_count(producer, len(offline_filled)):\n            with _assert_adapt_order_quantity_because_fees(_get_fees_for_currency) \\\n                as adapt_order_quantity_because_fees_mock:\n                await producer._ensure_staggered_orders()\n                await asyncio_tools.wait_asyncio_next_cycle()\n                assert adapt_order_quantity_because_fees_mock.call_count == 19\n        # restored orders (and create up to 50 orders as all orders can be created)\n        assert producer.operational_depth > orders_count\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        # did not replace orders: replace should not happen\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert sorted(new_orders, key=lambda x: x.origin_price)[-1] is sorted(open_orders, key=lambda x: x.origin_price)[-1]\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available < post_portfolio\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert all(\n            order.side == trading_enums.TradeOrderSide.SELL\n            for order in open_orders\n        )\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n\n        # trigger again\n        with _assert_missing_orders_count(producer, 1):\n            with _assert_adapt_order_quantity_because_fees(_get_fees_for_currency) \\\n                as adapt_order_quantity_because_fees_mock:\n                await producer._ensure_staggered_orders()\n                await asyncio_tools.wait_asyncio_next_cycle()\n                assert adapt_order_quantity_because_fees_mock.call_count == 1\n        # filled the grid with orders up to operational depth (45 as no sell order can be created bellow $5)\n        orders_count = 45\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        # did not replace orders: replace should not happen\n        new_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert sorted(new_orders, key=lambda x: x.origin_price)[-1] is sorted(open_orders, key=lambda x: x.origin_price)[-1]\n\n\nasync def test_start_after_offline_no_missing_order():\n    symbol = \"SOL/USDT\"\n    async with _get_tools(symbol) as (producer, _, exchange_manager):\n        producer.buy_funds = trading_constants.ZERO\n        producer.sell_funds = trading_constants.ZERO\n        producer.flat_spread = decimal.Decimal('0.714792')\n        producer.flat_increment = decimal.Decimal('0.34310016')\n        producer.buy_orders_count = 25\n        producer.sell_orders_count = 25\n        producer.enable_trailing_up = True\n        producer.enable_trailing_down = False\n        producer.use_existing_orders_only = False\n        producer.funds_redispatch_interval = 24\n        producer.use_existing_orders_only = False\n        producer.ignore_exchange_fees = True\n\n        pre_portfolio_usdt = trading_api.get_portfolio_currency(exchange_manager, \"USDT\")\n        pre_portfolio_sol = trading_api.get_portfolio_currency(exchange_manager, \"SOL\")\n        pre_portfolio_usdt.total = decimal.Decimal(\"59.25023354\")\n        pre_portfolio_usdt.available = pre_portfolio_usdt.total\n        pre_portfolio_sol.total = decimal.Decimal(\"0.397005\")\n        pre_portfolio_sol.available = pre_portfolio_sol.total\n\n        price = 148.736\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        open_orders = await open_orders_data.get_full_sol_usdt_open_orders(exchange_manager)\n        for order in open_orders:\n            await order.initialize()\n            await exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(order)\n\n        with mock.patch.object(producer, \"_create_not_virtual_orders\", mock.Mock()) as _create_not_virtual_orders_mock:\n\n            await producer._ensure_staggered_orders()\n            assert _create_not_virtual_orders_mock.call_count == 1\n            assert _create_not_virtual_orders_mock.mock_calls[0].args[0] == []\n            # should not find any missing order and should not trail\n\n\nasync def test_whole_grid_trailing_up_and_down():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, consumer, exchange_manager):\n        producer.use_order_by_order_trailing = False\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"1\")  # 19 buy orders\n        orders_count = 19 + 25\n\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100)\n        # A. price moves up\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        producer.enable_trailing_up = True\n\n        # top filled sell order price = 225\n        assert max(o.origin_price for o in offline_filled) == decimal.Decimal(\"225\")\n        new_price = decimal.Decimal(250)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price)\n        # will trail up\n        with mock.patch.object(\n            consumer, \"create_order\", mock.AsyncMock(wraps=consumer.create_order)\n        ) as create_order_mock, mock.patch.object(\n            producer.trading_mode, \"cancel_order\", mock.AsyncMock(wraps=producer.trading_mode.cancel_order)\n        ) as cancel_order_mock, mock.patch.object(\n            trading_modes, \"convert_asset_to_target_asset\", mock.AsyncMock(wraps=trading_modes.convert_asset_to_target_asset)\n        ) as convert_asset_to_target_asset_mock:\n            await producer._ensure_staggered_orders()\n            assert cancel_order_mock.call_count == 19 # all buy orders are cancelled\n            assert all(\n                call.kwargs[\"dependencies\"] is None\n                for call in cancel_order_mock.mock_calls\n            )\n            cancelled_orders_dependencies = trading_signals.get_orders_dependencies(\n                [call.args[0] for call in cancel_order_mock.mock_calls]\n            )\n            convert_asset_to_target_asset_mock.assert_not_called()\n            await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n            _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 250)\n            assert create_order_mock.call_count == producer.operational_depth\n            # no conversion, will use cancel order dependencies\n            assert all(\n                call.args[3] == cancelled_orders_dependencies\n                for call in create_order_mock.mock_calls\n            )\n\n        # B. orders get filled but not enough to trigger a trailing reset\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # all but 1 sell orders is filled\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL][:-1]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(offline_filled)\n        producer.enable_trailing_up = True\n        producer.enable_trailing_down = True\n        # doesn't trail up: a sell order still remains\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 250)\n        # all buy orders are still here\n        # not cancelled sell order is still here\n        offline_filled_ids = [o.order_id for o in offline_filled]\n        for order in open_orders:\n            if order.order_id in offline_filled_ids:\n                assert order.is_closed()\n            else:\n                assert order.is_open()\n\n        # C. price moves down, trailing down is disabled\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(offline_filled)\n        producer.enable_trailing_down = False\n\n        # top filled sell order price = 125\n        assert min(o.origin_price for o in offline_filled) == decimal.Decimal(\"125\")\n        new_price = decimal.Decimal(125)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price)\n        # will not trail down\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 250)\n        # only contains sell orders\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert all (order.side == trading_enums.TradeOrderSide.SELL for order in open_orders)\n\n        # D. price is still down, trailing down is enabled\n        producer.enable_trailing_down = True\n\n        # will trail down\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth - 1))   # -1 because the very first order can't be at a price <0\n        # orders are recreated around 125\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 125)\n        # now contains buy and sell orders\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        assert len([order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]) == producer.sell_orders_count\n        assert len([order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]) == producer.buy_orders_count - 1\n\n\nasync def test_order_by_order_trailing_up_and_down():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, consumer, exchange_manager):\n        producer.use_order_by_order_trailing = True\n        # first start: setup orders\n        producer.sell_funds = decimal.Decimal(\"1\")  # 25 sell orders\n        producer.buy_funds = decimal.Decimal(\"200\")  # 25 buy orders\n        orders_count = 25 + 25\n\n        price = 200\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == orders_count\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200)\n        # A. price moves up\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        producer.enable_trailing_up = True\n\n        # top filled sell order price = 325\n        assert max(o.origin_price for o in offline_filled) == decimal.Decimal(\"325\")\n        new_price = decimal.Decimal(\"350.1\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price)\n\n        _convert_asset_to_target_asset_returned_values = []\n        async def _convert_asset_to_target_asset(*args, **kwargs):\n            returned = await origin_convert_asset_to_target_asset(*args, **kwargs)\n            _convert_asset_to_target_asset_returned_values.append(returned)\n            return returned\n\n        origin_convert_asset_to_target_asset = trading_modes.convert_asset_to_target_asset\n        # will trail up\n        with mock.patch.object(\n            consumer, \"create_order\", mock.AsyncMock(wraps=consumer.create_order)\n        ) as create_order_mock, mock.patch.object(\n            producer.trading_mode, \"cancel_order\", mock.AsyncMock(wraps=producer.trading_mode.cancel_order)\n        ) as cancel_order_mock, mock.patch.object(\n            trading_modes, \"convert_asset_to_target_asset\", mock.AsyncMock(side_effect=_convert_asset_to_target_asset)\n        ) as convert_asset_to_target_asset_mock:\n            await producer._ensure_staggered_orders()\n            new_buy_order_prices_to_create = [\n                decimal.Decimal(\"325\"),\n                decimal.Decimal(\"330\"),\n                decimal.Decimal(\"335\"),\n                decimal.Decimal(\"340\"),\n                decimal.Decimal(\"345\"),\n            ]\n            cancelled_orders_prices = [\n                # replaced by new buy orders\n                decimal.Decimal(\"75\"),\n                decimal.Decimal(\"80\"),\n                decimal.Decimal(\"85\"),\n                decimal.Decimal(\"90\"),\n                decimal.Decimal(\"95\"),\n                # converted to BTC for the trailed sell order\n                decimal.Decimal(\"100\"),\n            ]\n            assert cancel_order_mock.call_count == len(cancelled_orders_prices)\n            assert sorted(\n                call.args[0].origin_price for call in cancel_order_mock.mock_calls\n            ) == cancelled_orders_prices\n            assert all(\n                call.kwargs[\"dependencies\"] is None\n                for call in cancel_order_mock.mock_calls\n            )\n            cancelled_orders_dependencies = trading_signals.get_orders_dependencies(\n                [call.args[0] for call in cancel_order_mock.mock_calls]\n            )\n            convert_asset_to_target_asset_mock.assert_awaited_once_with(\n                producer.trading_mode, \"USDT\", \"BTC\", {\n                    producer.symbol: {\n                        trading_enums.ExchangeConstantsTickersColumns.CLOSE.value: new_price,\n                    }\n                }, asset_amount=decimal.Decimal(\"7.7922\"),\n                dependencies=cancelled_orders_dependencies\n            )\n            convert_dependencies = trading_signals.get_orders_dependencies(\n                _convert_asset_to_target_asset_returned_values[-1]\n            )\n            await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n            _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 230)\n            assert create_order_mock.call_count == 25 + len(new_buy_order_prices_to_create) + 1 # replaced initial sell orders and created trailing buy orders + the \"other side\" order\n            assert sorted(\n                call.args[0].price \n                for call in create_order_mock.mock_calls\n            ) == sorted(\n                [ \n                    # replaced sell orders\n                    decimal.Decimal(str(i)) for i in range(200, 325, 5)\n                ]\n                # trailed orders\n                + new_buy_order_prices_to_create \n                # \"other side\" order\n                + [decimal.Decimal(\"355\")]\n            )\n            # no conversion, will use cancel order dependencies\n            assert all(\n                call.args[3] == convert_dependencies\n                for call in create_order_mock.mock_calls\n            )\n            open_orders = trading_api.get_open_orders(exchange_manager)\n            # ensure 1 sell order is open and the rest are buy orders\n            sell_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n            assert len(sell_orders) == 1\n            assert sell_orders[0].origin_price == decimal.Decimal(\"355\")\n            buy_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n            assert len(buy_orders) == orders_count - 1\n            assert sorted(\n                o.origin_price for o in buy_orders\n            ) == [\n                decimal.Decimal(str(i)) for i in range(105, 350, 5) # 105 to 345\n            ]\n\n        # B. single sell orders get filled, trail again \n        # offline simulation: buy orders get filled but not replaced\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # since 1 sell orders is filled\n        offline_filled = [\n            o \n            for o in open_orders \n            if o.side == trading_enums.TradeOrderSide.SELL\n        ]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        producer.enable_trailing_up = True\n        producer.enable_trailing_down = True\n        new_price = decimal.Decimal(\"360.1\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 240)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # ensure 1 sell order is open and the rest are buy orders\n        sell_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        assert len(sell_orders) == 1\n        assert sell_orders[0].origin_price == decimal.Decimal(\"365\")\n        assert sell_orders[0].origin_quantity == decimal.Decimal(\"0.02136986\")\n        buy_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        assert len(buy_orders) == orders_count - 1\n        assert sorted(\n            o.origin_price for o in buy_orders\n        ) == [\n            decimal.Decimal(str(i)) for i in range(115, 360, 5) # 115 to 355\n        ]\n\n        # C. price moves down, trailing down is disabled\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to more than the max price\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        producer.enable_trailing_down = False\n\n        # top filled sell order price = 125\n        assert min(o.origin_price for o in offline_filled) == decimal.Decimal(\"115\")\n        new_price = decimal.Decimal(\"114.9\")\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price)\n        # will not trail down\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 240)\n        # only contains sell orders\n        open_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert all (order.side == trading_enums.TradeOrderSide.SELL for order in open_orders)\n\n        # D. price is still down, trailing down is enabled\n        producer.enable_trailing_down = True\n\n        # will trail down\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        # orders trailed\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 235)\n        # now contains buy and sell orders\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # ensure 1 buy order is open and the rest are sell orders\n        sell_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        assert len(sell_orders) == orders_count - 1\n        assert sorted(\n            o.origin_price for o in sell_orders\n        ) == [\n            decimal.Decimal(str(i)) for i in range(120, 365, 5) # 120 to 360 (previous buy orders got replaced by sell orders at price+spread-increment)\n        ]\n        buy_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        assert len(buy_orders) == 1\n        assert buy_orders[0].origin_price == decimal.Decimal(\"110\")\n        assert buy_orders[0].origin_quantity == decimal.Decimal(\"0.07090908\")\n\n        # E. price is down much more, trail down on multiple orders\n        new_price = decimal.Decimal(\"82\")\n        offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY and o.origin_price > decimal.Decimal(\"82\")]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n        staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n        assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled)\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count))\n        # orders trailed\n        _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 205)\n        # now contains buy and sell orders\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        # ensure 1 buy order is open and the rest are sell orders\n        sell_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        assert len(sell_orders) == orders_count - 1\n        assert sorted(\n            o.origin_price for o in sell_orders\n        ) == [\n            decimal.Decimal(str(i)) for i in range(90, 335, 5) # 90 to 330\n        ]\n        buy_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        assert len(buy_orders) == 1\n        assert buy_orders[0].origin_price == decimal.Decimal(\"80\")\n        assert buy_orders[0].origin_quantity == decimal.Decimal(\"0.09887323\")\n\n\n@contextlib.contextmanager\ndef _assert_adapt_order_quantity_because_fees(get_fees_for_currency=False):\n    _origin_decimal_adapt_order_quantity_because_fees = trading_personal_data.decimal_adapt_order_quantity_because_fees\n\n    with mock.patch.object(\n        trading_personal_data, \"decimal_adapt_order_quantity_because_fees\",\n        mock.Mock(side_effect=_origin_decimal_adapt_order_quantity_because_fees)\n    ) as decimal_adapt_order_quantity_because_fees_mock:\n        if get_fees_for_currency is None:\n            yield decimal_adapt_order_quantity_because_fees_mock\n        else:\n            with mock.patch.object(\n                trading_personal_data, \"get_fees_for_currency\",\n                mock.Mock(side_effect=get_fees_for_currency)\n            ):\n                yield decimal_adapt_order_quantity_because_fees_mock\n\n\n@contextlib.contextmanager\ndef _assert_missing_orders_count(trading_mode_producer, expected_count):\n    origin_analyse_current_orders_situation = trading_mode_producer._analyse_current_orders_situation\n    missing_orders = []\n\n    def _local_analyse_current_orders_situation(*args, **kwargs):\n        return_vals = origin_analyse_current_orders_situation(*args, **kwargs)\n        created_missing_orders = return_vals[0]\n        for order in created_missing_orders:\n            missing_orders.append(order)\n        return return_vals\n\n    with mock.patch.object(trading_mode_producer, \"_analyse_current_orders_situation\", mock.Mock(\n        side_effect=_local_analyse_current_orders_situation\n    )) as _local_analyse_current_orders_situation_mock:\n        yield\n        _local_analyse_current_orders_situation_mock.assert_called_once()\n        assert len(missing_orders) == expected_count\n\n\nasync def _wait_for_orders_creation(orders_count=1):\n    for _ in range(orders_count):\n        await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def _check_open_orders_count(exchange_manager, count):\n    await _wait_for_orders_creation(count)\n    assert len(trading_api.get_open_orders(exchange_manager)) == count\n\n\nasync def _fill_order(order, exchange_manager, trigger_update_callback=True, producer=None):\n    initial_len = len(trading_api.get_open_orders(exchange_manager))\n    await order.on_fill(force_fill=True)\n    if order.status == trading_enums.OrderStatus.FILLED:\n        assert len(trading_api.get_open_orders(exchange_manager)) == initial_len - 1\n        if trigger_update_callback:\n            # Wait twice so allow `await asyncio_tools.wait_asyncio_next_cycle()` in order.initialize() to finish and complete\n            # order creation AND roll the next cycle that will wake up any pending portfolio lock and allow it to\n            # proceed (here `filled_order_state.terminate()` can be locked if an order has been previously filled AND\n            # a mirror order is being created (and its `await asyncio_tools.wait_asyncio_next_cycle()` in order.initialize()\n            # is pending: in this case `AbstractTradingModeConsumer.create_order_if_possible()` is still\n            # locking the portfolio cause of the previous order's `await asyncio_tools.wait_asyncio_next_cycle()`)).\n            # This lock issue can appear here because we don't use `asyncio_tools.wait_asyncio_next_cycle()` after mirror order\n            # creation (unlike anywhere else in this test file).\n            for _ in range(2):\n                await asyncio_tools.wait_asyncio_next_cycle()\n        else:\n            with mock.patch.object(producer, \"order_filled_callback\", new=mock.AsyncMock()):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n\ndef _check_created_orders(producer, orders, initial_price):\n    previous_order = None\n    sorted_orders = sorted(orders, key=lambda o: o.origin_price)\n    for order in sorted_orders:\n        # price\n        if previous_order:\n            if previous_order.side == order.side:\n                assert order.origin_price == previous_order.origin_price + producer.flat_increment\n            else:\n                assert order.origin_price == previous_order.origin_price + producer.flat_spread\n        previous_order = order\n    min_price = max(\n        0,\n        decimal.Decimal(str(initial_price)) - producer.flat_spread / 2\n        - (producer.flat_increment * (producer.buy_orders_count - 1))\n    )\n    max_price = decimal.Decimal(str(initial_price)) + producer.flat_spread / 2 + \\\n                (producer.flat_increment * (producer.sell_orders_count - 1))\n    assert min_price <= sorted_orders[0].origin_price <= max_price, (\n        f\"min_price: {min_price}, {sorted_orders[0].origin_price=}, max_price: {max_price}\"\n    )\n    assert min_price <= sorted_orders[-1].origin_price <= max_price, (\n        f\"min_price: {min_price}, {sorted_orders[-1].origin_price=}, max_price: {max_price}\"\n    )\n"
  },
  {
    "path": "Trading/Mode/index_trading_mode/__init__.py",
    "content": "from .index_trading import IndexTradingMode"
  },
  {
    "path": "Trading/Mode/index_trading_mode/config/IndexTradingMode.json",
    "content": "{\n    \"required_strategies\": [],\n    \"refresh_interval\": 1,\n    \"rebalance_trigger_min_percent\": 5,\n    \"index_content\": []\n}"
  },
  {
    "path": "Trading/Mode/index_trading_mode/index_distribution.py",
    "content": "import decimal\nimport typing\nimport numpy\n\nimport octobot_trading.constants\n\nDISTRIBUTION_NAME = \"name\"\nDISTRIBUTION_VALUE = \"value\"\nMAX_DISTRIBUTION_AFTER_COMMA_DIGITS = 1\n\n\ndef get_uniform_distribution(coins) -> typing.List:\n    if not coins:\n        return []\n    ratio = float(\n        round(\n            octobot_trading.constants.ONE / decimal.Decimal(str(len(coins))) * octobot_trading.constants.ONE_HUNDRED,\n            MAX_DISTRIBUTION_AFTER_COMMA_DIGITS\n        )\n    )\n    if not ratio:\n        return []\n    return [\n        {\n            DISTRIBUTION_NAME: coin,\n            DISTRIBUTION_VALUE: ratio\n        }\n        for coin in coins\n    ]\n\n\ndef get_linear_distribution(weight_by_coin: dict[str, decimal.Decimal]) -> typing.List:\n    total_weight = sum(weight for weight in weight_by_coin.values())\n    if total_weight <= octobot_trading.constants.ZERO:\n        raise ValueError(f\"total weight is {total_weight}\")\n    return [\n        {\n            DISTRIBUTION_NAME: coin,\n            DISTRIBUTION_VALUE: float(round(\n                weight / total_weight * octobot_trading.constants.ONE_HUNDRED,\n                MAX_DISTRIBUTION_AFTER_COMMA_DIGITS\n            ))\n        }\n        for coin, weight in weight_by_coin.items()\n    ]\n\n\ndef get_smoothed_distribution(weight_by_coin: dict[str, decimal.Decimal]) -> typing.List:\n    return get_linear_distribution({\n        coin: decimal.Decimal(str(numpy.cbrt(float(weight))))\n        for coin, weight in weight_by_coin.items()\n    })\n"
  },
  {
    "path": "Trading/Mode/index_trading_mode/index_trading.py",
    "content": "#  Drakkar-Software OctoBot\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport decimal\nimport enum\nimport typing\n\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.authentication as authentication\nimport octobot_commons.signals as commons_signals\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.util as trading_util\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.signals as signals\n\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\n\n\nclass IndexActivity(enum.Enum):\n    REBALANCING_DONE = \"rebalancing_done\"\n    REBALANCING_SKIPPED = \"rebalancing_skipped\"\n\n\nclass RebalanceSkipDetails(enum.Enum):\n    ALREADY_BALANCED = \"already_balanced\"\n    NOT_ENOUGH_AVAILABLE_FOUNDS = \"not_enough_available_founds\"\n\n\nclass RebalanceDetails(enum.Enum):\n    SELL_SOME = \"SELL_SOME\"\n    BUY_MORE = \"BUY_MORE\"\n    REMOVE = \"REMOVE\"\n    ADD = \"ADD\"\n    SWAP = \"SWAP\"\n    FORCED_REBALANCE = \"FORCED_REBALANCE\"\n\n\nclass SynchronizationPolicy(enum.Enum):\n    SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE = \"sell_removed_index_coins_on_ratio_rebalance\"\n    SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE = \"sell_removed_index_coins_as_soon_as_possible\"\n\n\nclass RebalanceAborted(Exception):\n    pass\n\n\nDEFAULT_QUOTE_ASSET_REBALANCE_TRIGGER_MIN_RATIO = 0.1  # 10%\nDEFAULT_REBALANCE_TRIGGER_MIN_RATIO = 0.05  # 5%\n\n\nclass IndexTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    FILL_ORDER_TIMEOUT = 60\n    SIMPLE_ADD_MIN_TOLERANCE_RATIO = decimal.Decimal(\"0.8\")  # 20% tolerance\n\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self._already_logged_aborted_rebalance_error = False\n\n    async def create_new_orders(self, symbol, _, state, **kwargs):\n        details = kwargs[self.CREATE_ORDER_DATA_PARAM]\n        dependencies = kwargs.get(self.CREATE_ORDER_DEPENDENCIES_PARAM, None)\n        if state == trading_enums.EvaluatorStates.NEUTRAL.value:\n            try:\n                self.trading_mode.is_processing_rebalance = True\n                return await self._rebalance_portfolio(details, dependencies)\n            finally:\n                self.trading_mode.is_processing_rebalance = False\n        self.logger.error(f\"Unknown index state: {state}\")\n        return []\n\n    async def _rebalance_portfolio(self, details: dict, initial_dependencies: typing.Optional[commons_signals.SignalDependencies]):\n        self.logger.info(f\"Executing rebalance on [{self.exchange_manager.exchange_name}]\")\n        orders = []\n        try:\n            # 1. make sure we can actually rebalance the portfolio\n            self.logger.info(\"Step 1/3: ensuring enough funds are available for rebalance\")\n            await self._ensure_enough_funds_to_buy_after_selling()\n            # 2. sell indexed coins for reference market\n            is_simple_buy_without_selling = self._can_simply_buy_coins_without_selling(details)\n            sell_orders_dependencies = initial_dependencies\n            if is_simple_buy_without_selling:\n                self.logger.info(\n                    f\"Step 2/3: skipped: no coin to sell for \"\n                    f\"{self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market}\"\n                )\n            else:\n                self.logger.info(\n                    f\"Step 2/3: selling coins to free \"\n                    f\"{self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market}\"\n                )\n                orders += await self._sell_indexed_coins_for_reference_market(details, initial_dependencies)\n                sell_orders_dependencies = signals.get_orders_dependencies(orders)\n            # 3. split reference market into indexed coins\n            self.logger.info(\n                f\"Step 3/3: buying coins using \"\n                f\"{self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market}\"\n            )\n            orders += await self._split_reference_market_into_indexed_coins(\n                details, is_simple_buy_without_selling, sell_orders_dependencies\n            )\n            # reset flag to relog if a next rebalance is aborted\n            self._already_logged_aborted_rebalance_error = False\n        except (trading_errors.MissingMinimalExchangeTradeVolume, RebalanceAborted) as err:\n            log_level = self.logger.warning\n            if isinstance(err, RebalanceAborted) and not self._already_logged_aborted_rebalance_error:\n                log_level = self.logger.error\n                self._already_logged_aborted_rebalance_error = True\n            log_level(\n                f\"Aborting rebalance on {self.exchange_manager.exchange_name}: {err} ({err.__class__.__name__})\"\n            )\n            self._update_producer_last_activity(\n                IndexActivity.REBALANCING_SKIPPED,\n                RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n            )\n        finally:\n            self.logger.info(\"Portoflio rebalance process complete\")\n        return orders\n\n    async def _sell_indexed_coins_for_reference_market(\n        self, details: dict, dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ) -> list:\n        removed_coins_to_sell_orders = []\n        if removed_coins_to_sell := list(details[RebalanceDetails.REMOVE.value]):\n            removed_coins_to_sell_orders = await trading_modes.convert_assets_to_target_asset(\n                self.trading_mode, removed_coins_to_sell,\n                self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market, {},\n                dependencies=dependencies\n            )\n            if (\n                details[RebalanceDetails.REMOVE.value] and\n                not (\n                    details[RebalanceDetails.BUY_MORE.value]\n                    or details[RebalanceDetails.ADD.value]\n                    or details[RebalanceDetails.SWAP.value]\n                )\n            ):\n                # if rebalance is triggered by removed assets, make sure that the asset can actually be sold\n                # otherwise the whole rebalance is useless\n                sold_coins = [\n                    symbol_util.parse_symbol(order.symbol).base\n                    if order.side is trading_enums.TradeOrderSide.SELL\n                    else symbol_util.parse_symbol(order.symbol).quote\n                    for order in removed_coins_to_sell_orders\n                ]\n                if not any(\n                    asset in sold_coins\n                    for asset in details[RebalanceDetails.REMOVE.value]\n                ):\n                    self.logger.info(\n                        f\"Cancelling rebalance: not enough {list(details[RebalanceDetails.REMOVE.value])} funds to sell\"\n                    )\n                    raise trading_errors.MissingMinimalExchangeTradeVolume(\n                        f\"not enough {list(details[RebalanceDetails.REMOVE.value])} funds to sell\"\n                    )\n        order_coins_to_sell = self._get_coins_to_sell(details)\n        orders = await trading_modes.convert_assets_to_target_asset(\n            self.trading_mode, order_coins_to_sell,\n            self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market, {},\n            dependencies=dependencies\n        ) + removed_coins_to_sell_orders\n        if orders:\n            # ensure orders are filled\n            await asyncio.gather(\n                *[\n                    trading_personal_data.wait_for_order_fill(\n                        order, self.FILL_ORDER_TIMEOUT, True\n                    ) for order in orders\n                ]\n            )\n        return orders\n\n    def _get_coins_to_sell(self, details: dict) -> list:\n        return list(details[RebalanceDetails.SWAP.value]) or (\n            self.trading_mode.indexed_coins\n        )\n\n    def _can_simply_buy_coins_without_selling(self, details: dict) -> bool:\n        simple_buy_coins = self._get_simple_buy_coins(details)\n        if not simple_buy_coins:\n            return False\n        # check if there is enough free funds to buy those coins\n        ref_market = self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n        reference_market_to_split = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n            portfolio_value_holder.get_traded_assets_holdings_value(ref_market, None)\n        free_reference_market_holding = \\\n            self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n                ref_market\n            ).available\n        cumulated_ratio = sum(\n            self.trading_mode.get_target_ratio(coin)\n            for coin in simple_buy_coins\n        )\n        tolerated_min_amount = reference_market_to_split * cumulated_ratio * self.SIMPLE_ADD_MIN_TOLERANCE_RATIO\n        # can reach target ratios without selling if this condition is met\n        return tolerated_min_amount <= free_reference_market_holding\n\n\n    def _get_simple_buy_coins(self, details: dict) -> list:\n        # Returns the list of coins to simply buy.\n        # Used to avoid a full rebalance when coins are seen as added to a basket\n        # AND funds are available to buy it AND no asset should be sold\n        added = details[RebalanceDetails.ADD.value] or details[RebalanceDetails.BUY_MORE.value]\n        if added and not (\n            details[RebalanceDetails.SWAP.value]\n            or details[RebalanceDetails.SELL_SOME.value]\n            or details[RebalanceDetails.REMOVE.value]\n            or details[RebalanceDetails.FORCED_REBALANCE.value]\n        ):\n            added_coins = list(details[RebalanceDetails.ADD.value]) + list(details[RebalanceDetails.BUY_MORE.value])\n            return [\n                coin\n                for coin in self.trading_mode.indexed_coins # iterate over self.trading_mode.indexed_coins to keep order\n                if coin in added_coins\n            ] + [\n                coin\n                for coin in added_coins\n                if coin not in self.trading_mode.indexed_coins\n            ]\n        return []\n\n    async def _ensure_enough_funds_to_buy_after_selling(self):\n        reference_market_to_split = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n            portfolio_value_holder.get_traded_assets_holdings_value(\n                self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market, None\n            )\n        # will raise if funds are missing\n        await self._get_symbols_and_amounts(self.trading_mode.indexed_coins, reference_market_to_split)\n\n    async def _split_reference_market_into_indexed_coins(\n        self, details: dict, is_simple_buy_without_selling: bool, dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ):\n        orders = []\n        ref_market = self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n        if details[RebalanceDetails.SWAP.value] or is_simple_buy_without_selling:\n            # has to infer total reference market holdings\n            reference_market_to_split = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                portfolio_value_holder.get_traded_assets_holdings_value(ref_market, None)\n            coins_to_buy = (\n                self._get_simple_buy_coins(details) if is_simple_buy_without_selling\n                else list(details[RebalanceDetails.SWAP.value].values())\n            )\n        else:\n            # can use actual reference market holdings: everything has been sold\n            reference_market_to_split = \\\n                self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n                    ref_market\n                ).available\n            coins_to_buy = self.trading_mode.indexed_coins\n        self.logger.info(f\"Splitting {reference_market_to_split} {ref_market} to buy {coins_to_buy}\")\n        amount_by_symbol = await self._get_symbols_and_amounts(coins_to_buy, reference_market_to_split)\n        for symbol, ideal_amount in amount_by_symbol.items():\n            orders.extend(await self._buy_coin(symbol, ideal_amount, dependencies))\n        if not orders:\n            raise trading_errors.MissingMinimalExchangeTradeVolume()\n        return orders\n\n    async def _get_symbols_and_amounts(self, coins_to_buy, reference_market_to_split):\n        amount_by_symbol = {}\n        for coin in coins_to_buy:\n            if coin == self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market:\n                # nothing to do for reference market, keep as is\n                continue\n            symbol = symbol_util.merge_currencies(\n                coin,\n                self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n            )\n            price = await trading_personal_data.get_up_to_date_price(\n                self.exchange_manager, symbol, timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT\n            )\n            symbol_market = self.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n            ratio = self.trading_mode.get_target_ratio(coin)\n            if ratio == trading_constants.ZERO:\n                # coin is not to handle\n                continue\n            try:\n                ideal_amount = ratio * reference_market_to_split / price\n            except decimal.DecimalException as err:\n                raise RebalanceAborted(\n                    f\"Error computing {symbol} ideal amount ({ratio=}, {reference_market_to_split=}, {price=}): {err=}\"\n                ) from err\n            # worse case (ex with 5 USDT min order size): exactly 5 USDT can be in portfolio, we therefore want to\n            # trade at lease 5 USDT to be able to buy more.\n            # - we want ideal_amount - min_cost > min_cost\n            # - in other words ideal_amount > 2*min_cost => ideal_amount/2 > min cost\n            adapted_quantity = trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                ideal_amount / decimal.Decimal(2),\n                price,\n                symbol_market\n            )\n            if not adapted_quantity:\n                # if we can't create an order in this case, we won't be able to balance the portfolio.\n                # don't try to avoid triggering new rebalances on each wakeup cycling market sell & buy orders\n                raise trading_errors.MissingMinimalExchangeTradeVolume(\n                    f\"Can't buy {symbol}: available funds are too low to buy {ratio*trading_constants.ONE_HUNDRED}% \"\n                    f\"of {reference_market_to_split} holdings: {round(ideal_amount / decimal.Decimal(2), 9)} {coin} \"\n                    f\"required order size is not compatible with {symbol} exchange requirements: \"\n                    f\"{symbol_market[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value]}.\"\n                )\n            amount_by_symbol[symbol] = ideal_amount\n        return amount_by_symbol\n\n    async def _buy_coin(self, symbol, ideal_amount, dependencies: typing.Optional[commons_signals.SignalDependencies]) -> list:\n        current_symbol_holding, current_market_holding, market_quantity, price, symbol_market = \\\n            await trading_personal_data.get_pre_order_data(\n                self.exchange_manager, symbol=symbol, timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT\n            )\n        order_target_price = price\n        # ideally use the expected reference_market_available_holdings ratio, fallback to available\n        # holdings if necessary\n        target_quantity = min(ideal_amount, current_market_holding / order_target_price)\n        ideal_quantity = target_quantity - current_symbol_holding\n        if ideal_quantity <= trading_constants.ZERO:\n            return []\n        quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n            self.exchange_manager, symbol, trading_enums.TraderOrderType.BUY_MARKET, ideal_quantity,\n            order_target_price, trading_enums.TradeOrderSide.BUY\n        )\n        created_orders = []\n        orders_should_have_been_created = False\n        ideal_order_type = trading_enums.TraderOrderType.BUY_MARKET\n        order_type = (\n            ideal_order_type\n            if self.exchange_manager.exchange.is_market_open_for_order_type(symbol, ideal_order_type)\n            else trading_enums.TraderOrderType.BUY_LIMIT\n        )\n\n        if trading_personal_data.get_trade_order_type(order_type) is not trading_enums.TradeOrderType.MARKET:\n            # can't use market orders: use limit orders with price a bit above the current price to instant fill it.\n            order_target_price, quantity = \\\n                trading_modes.get_instantly_filled_limit_order_adapted_price_and_quantity(\n                    order_target_price, quantity, order_type\n                )\n        for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n            quantity,\n            order_target_price,\n            symbol_market\n        ):\n            orders_should_have_been_created = True\n            current_order = trading_personal_data.create_order_instance(\n                trader=self.exchange_manager.trader,\n                order_type=order_type,\n                symbol=symbol,\n                current_price=price,\n                quantity=order_quantity,\n                price=order_price,\n            )\n            created_order = await self.trading_mode.create_order(current_order, dependencies=dependencies)\n            created_orders.append(created_order)\n        if created_orders:\n            return created_orders\n        if orders_should_have_been_created:\n            raise trading_errors.OrderCreationError()\n        raise trading_errors.MissingMinimalExchangeTradeVolume()\n\n\nclass IndexTradingModeProducer(trading_modes.AbstractTradingModeProducer):\n    REFRESH_INTERVAL = \"refresh_interval\"\n    CANCEL_OPEN_ORDERS = \"cancel_open_orders\"\n    REBALANCE_TRIGGER_MIN_PERCENT = \"rebalance_trigger_min_percent\"\n    SELECTED_REBALANCE_TRIGGER_PROFILE = \"selected_rebalance_trigger_profile\"\n    REBALANCE_TRIGGER_PROFILES = \"rebalance_trigger_profiles\"\n    REBALANCE_TRIGGER_PROFILE_NAME = \"name\"\n    REBALANCE_TRIGGER_PROFILE_MIN_PERCENT = \"min_percent\"\n    QUOTE_ASSET_REBALANCE_TRIGGER_MIN_PERCENT = \"quote_asset_rebalance_trigger_min_percent\"\n    SYNCHRONIZATION_POLICY = \"synchronization_policy\"\n    SELL_UNINDEXED_TRADED_COINS = \"sell_unindexed_traded_coins\"\n    INDEX_CONTENT = \"index_content\"\n    MIN_INDEXED_COINS = 1\n    ALLOWED_1_TO_1_SWAP_COUNTS = 1\n    MIN_RATIO_TO_SELL = decimal.Decimal(\"0.0001\")  # 1/10000\n    QUOTE_ASSET_TO_INDEXED_SWAP_RATIO_THRESHOLD = decimal.Decimal(\"0.1\")  # 10%\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        self._last_trigger_time = 0\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        await super().stop()\n\n    async def ohlcv_callback(self, exchange: str, exchange_id: str, cryptocurrency: str, symbol: str,\n                             time_frame: str, candle: dict, init_call: bool = False):\n        await self._check_index_if_necessary()\n\n    async def kline_callback(self, exchange: str, exchange_id: str, cryptocurrency: str, symbol: str,\n                             time_frame, kline: dict):\n        await self._check_index_if_necessary()\n\n    async def _check_index_if_necessary(self):\n        current_time = self.exchange_manager.exchange.get_exchange_current_time()\n        if (\n            current_time - self._last_trigger_time\n        ) >= self.trading_mode.refresh_interval_days * commons_constants.DAYS_TO_SECONDS:\n            if self.trading_mode.automatically_update_historical_config_on_set_intervals():\n                self.trading_mode.update_config_and_user_inputs_if_necessary()\n            if self.trading_mode.is_processing_rebalance:\n                self.logger.info(\n                    f\"[{self.exchange_manager.exchange_name}] Index is already being rebalanced, skipping index check\"\n                )\n                return\n            if len(self.trading_mode.indexed_coins) < self.MIN_INDEXED_COINS:\n                self.logger.error(\n                    f\"At least {self.MIN_INDEXED_COINS} coin is required to maintain an index. Please \"\n                    f\"select more trading pairs using \"\n                    f\"{self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market} as \"\n                    f\"quote currency.\"\n                )\n            else:\n                self._notify_if_missing_too_many_coins()\n                await self.ensure_index()\n            if not self.trading_mode.is_updating_at_each_price_change():\n                self.logger.debug(f\"Next index check in {self.trading_mode.refresh_interval_days} days\")\n            self._last_trigger_time = current_time\n\n    async def ensure_index(self):\n        await self._wait_for_symbol_prices_and_profitability_init(self._get_config_init_timeout())\n        self.logger.info(\n            f\"Ensuring Index on [{self.exchange_manager.exchange_name}] \"\n            f\"{len(self.trading_mode.indexed_coins)} coins: {self.trading_mode.indexed_coins} with reference market: \"\n            f\"{self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market}\"\n        )\n        dependencies = None\n        if self.trading_mode.cancel_open_orders:\n            dependencies = await self.cancel_traded_pairs_open_orders_if_any()\n        if self.trading_mode.requires_initializing_appropriate_coins_distribution:\n            self.trading_mode.ensure_updated_coins_distribution(adapt_to_holdings=True)\n            self.trading_mode.requires_initializing_appropriate_coins_distribution = False\n        is_rebalance_required, rebalance_details = self._get_rebalance_details()\n        if is_rebalance_required:\n            await self._trigger_rebalance(rebalance_details, dependencies)\n            self.last_activity = trading_modes.TradingModeActivity(\n                IndexActivity.REBALANCING_DONE,\n                rebalance_details,\n            )\n        else:\n            allowance = round(self.trading_mode.rebalance_trigger_min_ratio * trading_constants.ONE_HUNDRED, 2)\n            self.logger.info(\n                f\"[{self.exchange_manager.exchange_name}] is following the index [+/-{allowance}%], no rebalance is required.\"\n            )\n            self.last_activity = trading_modes.TradingModeActivity(IndexActivity.REBALANCING_SKIPPED)\n\n    async def _trigger_rebalance(self, rebalance_details: dict, dependencies: typing.Optional[commons_signals.SignalDependencies]):\n        self.logger.info(\n            f\"Triggering rebalance on [{self.exchange_manager.exchange_name}] \"\n            f\"with rebalance details: {rebalance_details}.\"\n        )\n        await self.submit_trading_evaluation(\n            cryptocurrency=None,\n            symbol=None,    # never set symbol in order to skip consumer.can_create_order check\n            time_frame=None,\n            final_note=None,\n            state=trading_enums.EvaluatorStates.NEUTRAL,\n            data=rebalance_details,\n            dependencies=dependencies\n        )\n        # send_notification\n        await self._send_alert_notification()\n\n    async def _send_alert_notification(self):\n        if self.exchange_manager.is_backtesting:\n            return\n        try:\n            import octobot_services.api as services_api\n            import octobot_services.enums as services_enum\n            title = \"Index trigger\"\n            alert = f\"Rebalance triggered for {len(self.trading_mode.indexed_coins)} coins\"\n            await services_api.send_notification(services_api.create_notification(\n                alert, title=title, markdown_text=alert,\n                category=services_enum.NotificationCategory.PRICE_ALERTS\n            ))\n        except ImportError as e:\n            self.logger.exception(e, True, f\"Impossible to send notification: {e}\")\n\n    def _notify_if_missing_too_many_coins(self):\n        if ideal_distribution := self.trading_mode.get_ideal_distribution(self.trading_mode.trading_config):\n            if len(self.trading_mode.indexed_coins) < len(ideal_distribution) / 2:\n                self.logger.error(\n                    f\"Less than half of configured coins can be traded on {self.exchange_manager.exchange_name}. \"\n                    f\"Traded: {self.trading_mode.indexed_coins}, configured: {ideal_distribution}\"\n                )\n\n    def _register_coins_update(self, rebalance_details: dict) -> bool:\n        should_rebalance = False\n        for coin in set(self.trading_mode.indexed_coins):\n            target_ratio = self.trading_mode.get_target_ratio(coin)\n            coin_ratio = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                portfolio_value_holder.get_holdings_ratio(\n                    coin, traded_symbols_only=True\n                )\n            beyond_ratio = True\n            if coin_ratio == trading_constants.ZERO and target_ratio > trading_constants.ZERO:\n                # missing coin in portfolio\n                rebalance_details[RebalanceDetails.ADD.value][coin] = target_ratio\n                should_rebalance = True\n            elif coin_ratio < target_ratio - self.trading_mode.rebalance_trigger_min_ratio:\n                # not enough in portfolio\n                rebalance_details[RebalanceDetails.BUY_MORE.value][coin] = target_ratio\n                should_rebalance = True\n            elif coin_ratio > target_ratio + self.trading_mode.rebalance_trigger_min_ratio:\n                # too much in portfolio\n                rebalance_details[RebalanceDetails.SELL_SOME.value][coin] = target_ratio\n                should_rebalance = True\n            else:\n                beyond_ratio = False\n            if beyond_ratio:\n                allowance = round(self.trading_mode.rebalance_trigger_min_ratio * trading_constants.ONE_HUNDRED, 2)\n                self.logger.info(\n                    f\"{coin} is beyond the target ratio of {round(target_ratio * trading_constants.ONE_HUNDRED, 2)}[+/-{allowance}]%, \"\n                    f\"ratio: {round(coin_ratio * trading_constants.ONE_HUNDRED, 2)}%. A rebalance is required.\"\n                )\n        return should_rebalance\n\n    def _register_removed_coin(self, rebalance_details: dict, available_traded_bases: set[str]) -> bool:\n        should_rebalance = False\n        for coin in self.trading_mode.get_removed_coins_from_config(available_traded_bases):\n            if coin in available_traded_bases:\n                coin_ratio = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                    portfolio_value_holder.get_holdings_ratio(\n                        coin, traded_symbols_only=True\n                    )\n                if coin_ratio >= self.MIN_RATIO_TO_SELL:\n                    # coin to sell in portfolio\n                    rebalance_details[RebalanceDetails.REMOVE.value][coin] = coin_ratio\n                    self.logger.info(\n                        f\"{coin} (holdings: {round(coin_ratio * trading_constants.ONE_HUNDRED, 3)}%) is not in index \"\n                        f\"anymore. A rebalance is required.\"\n                    )\n                    should_rebalance = True\n            else:\n                if trading_util.is_symbol_disabled(self.exchange_manager.config, coin):\n                    self.logger.info(\n                        f\"Ignoring {coin} holding: {coin} is not in index anymore but is disabled.\"\n                    )\n                else:\n                    self.logger.error(\n                        f\"Ignoring {coin} holding: Can't sell {coin} as it is not in any trading pair\"\n                        f\" but is not in index anymore. This is unexpected\"\n                    )\n        return should_rebalance\n\n    def _register_quote_asset_rebalance(self, rebalance_details: dict) -> bool:\n        non_indexed_quote_assets_ratio = self._get_non_indexed_quote_assets_ratio()\n        if self._should_rebalance_due_to_non_indexed_quote_assets_ratio(\n            non_indexed_quote_assets_ratio, rebalance_details\n        ):\n            rebalance_details[RebalanceDetails.FORCED_REBALANCE.value] = True\n            self.logger.info(\n                f\"Rebalancing due to a high non-indexed quote asset holdings ratio: \"\n                f\"{round(non_indexed_quote_assets_ratio * trading_constants.ONE_HUNDRED, 2)}%, quote rebalance \"\n                f\"threshold = {self.trading_mode.quote_asset_rebalance_ratio_threshold * trading_constants.ONE_HUNDRED}%\"\n            )\n            return True\n        return False\n    \n    def _empty_rebalance_details(self) -> dict:\n        return {\n            RebalanceDetails.SELL_SOME.value: {},\n            RebalanceDetails.BUY_MORE.value: {},\n            RebalanceDetails.REMOVE.value: {},\n            RebalanceDetails.ADD.value: {},\n            RebalanceDetails.SWAP.value: {},\n            RebalanceDetails.FORCED_REBALANCE.value: False,\n        }\n\n    def _get_rebalance_details(self) -> (bool, dict):\n        rebalance_details = self._empty_rebalance_details()\n        should_rebalance = False\n        # look for coins update in indexed_coins\n        available_traded_bases = set(\n            symbol.base\n            for symbol in self.exchange_manager.exchange_config.traded_symbols\n        )\n\n        # compute rebalance details for current coins distribution\n        if self.trading_mode.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE:\n            should_rebalance = self._register_removed_coin(rebalance_details, available_traded_bases)\n        should_rebalance = self._register_coins_update(rebalance_details) or should_rebalance\n        should_rebalance = self._register_quote_asset_rebalance(rebalance_details) or should_rebalance\n        if (\n            should_rebalance \n            and self.trading_mode.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n        ):\n            # use latest coins distribution to compute rebalance details\n            self.trading_mode.ensure_updated_coins_distribution(force_latest=True)\n            # re-compute the whole rebalance details for latest coins distribution \n            # to avoid side effects from previous distribution\n            rebalance_details = self._empty_rebalance_details()\n            self._register_removed_coin(rebalance_details, available_traded_bases)\n            self._register_coins_update(rebalance_details)\n            self._register_quote_asset_rebalance(rebalance_details)\n\n        if not rebalance_details[RebalanceDetails.FORCED_REBALANCE.value]:\n            # finally, compute swaps when no forced rebalance is required\n            self._resolve_swaps(rebalance_details)\n            for origin, target in rebalance_details[RebalanceDetails.SWAP.value].items():\n                origin_ratio = round(\n                    rebalance_details[RebalanceDetails.REMOVE.value][origin] * trading_constants.ONE_HUNDRED,\n                    3\n                )\n                target_ratio = round(\n                    rebalance_details[RebalanceDetails.ADD.value].get(\n                        target,\n                        rebalance_details[RebalanceDetails.BUY_MORE.value].get(target, trading_constants.ZERO)\n                    ) * trading_constants.ONE_HUNDRED,\n                    3\n                ) or \"???\"\n                self.logger.info(\n                    f\"Swapping {origin} (holding ratio: {origin_ratio}%) for {target} (to buy ratio: {target_ratio}%) \"\n                    f\"on [{self.exchange_manager.exchange_name}]: ratios are similar enough to allow swapping.\"\n                )   \n        return (should_rebalance or rebalance_details[RebalanceDetails.FORCED_REBALANCE.value]), rebalance_details\n\n    def _should_rebalance_due_to_non_indexed_quote_assets_ratio(self, non_indexed_quote_assets_ratio: decimal.Decimal, rebalance_details: dict) -> bool:\n        total_added_ratio = (\n            self._sum_ratios(rebalance_details, RebalanceDetails.ADD.value) \n            + self._sum_ratios(rebalance_details, RebalanceDetails.BUY_MORE.value)\n        )\n        \n        if (\n            total_added_ratio * (trading_constants.ONE - self.QUOTE_ASSET_TO_INDEXED_SWAP_RATIO_THRESHOLD)\n            <= non_indexed_quote_assets_ratio\n            <= total_added_ratio * (trading_constants.ONE + self.QUOTE_ASSET_TO_INDEXED_SWAP_RATIO_THRESHOLD)\n        ):\n            total_removed_ratio = (\n                self._sum_ratios(rebalance_details, RebalanceDetails.REMOVE.value) \n                + self._sum_ratios(rebalance_details, RebalanceDetails.SELL_SOME.value)\n            )\n            # added coins are equivalent to free quote assets: just buy with quote assets\n            if total_removed_ratio == trading_constants.ZERO:\n                return False\n        # there are removed coins or added ratio is not equivalent to free quote assets: rebalance if necessary\n        min_ratio = min(\n            min(\n                self.trading_mode.get_target_ratio(coin)\n                for coin in self.trading_mode.indexed_coins\n            ) if self.trading_mode.indexed_coins else self.trading_mode.quote_asset_rebalance_ratio_threshold,\n            self.trading_mode.quote_asset_rebalance_ratio_threshold\n        )\n        return non_indexed_quote_assets_ratio >= min_ratio\n\n    @staticmethod\n    def _sum_ratios(rebalance_details: dict, key: str) -> decimal.Decimal:\n        return decimal.Decimal(str(sum(\n            ratio\n            for ratio in rebalance_details[key].values()\n        ))) if rebalance_details[key] else trading_constants.ZERO \n\n    def _get_non_indexed_quote_assets_ratio(self) -> decimal.Decimal:\n        return decimal.Decimal(str(sum(\n            self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                portfolio_value_holder.get_holdings_ratio(\n                    quote, traded_symbols_only=True\n                )\n            for quote in set(\n                symbol.quote\n                for symbol in self.exchange_manager.exchange_config.traded_symbols\n                if symbol.quote not in self.trading_mode.indexed_coins\n            )\n        )))\n\n    def _resolve_swaps(self, details: dict):\n        removed = details[RebalanceDetails.REMOVE.value]\n        details[RebalanceDetails.SWAP.value] = {}\n        if details[RebalanceDetails.SELL_SOME.value]:\n            # rebalance within held coins: global rebalance required\n            return\n        added = {**details[RebalanceDetails.ADD.value], **details[RebalanceDetails.BUY_MORE.value]}\n        if len(removed) == len(added) == self.ALLOWED_1_TO_1_SWAP_COUNTS:\n            for removed_coin, removed_ratio, added_coin, added_ratio in zip(\n                removed, removed.values(), added, added.values()\n            ):\n                added_holding_ratio = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                    portfolio_value_holder.get_holdings_ratio(\n                        added_coin, traded_symbols_only=True,\n                        coins_whitelist=self.trading_mode.get_coins_to_consider_for_ratio()\n                    )\n                required_added_ratio = added_ratio - added_holding_ratio\n                if (\n                    removed_ratio - self.trading_mode.rebalance_trigger_min_ratio\n                    < required_added_ratio\n                    < removed_ratio + self.trading_mode.rebalance_trigger_min_ratio\n                ):\n                    # removed can be swapped for added: only sell removed\n                    details[RebalanceDetails.SWAP.value][removed_coin] = added_coin\n                else:\n                    # reset to_sell to sell everything\n                    details[RebalanceDetails.SWAP.value] = {}\n                    return\n\n    def get_channels_registration(self):\n        # use candles to trigger at each candle interval and when initializing\n        topics = [\n            self.TOPIC_TO_CHANNEL_NAME[commons_enums.ActivationTopics.FULL_CANDLES.value],\n        ]\n        if self.trading_mode.is_updating_at_each_price_change():\n            # use kline to trigger at each price change\n            self.logger.info(f\"Using price change bound update instead of time-based update.\")\n            topics.append(\n                self.TOPIC_TO_CHANNEL_NAME[commons_enums.ActivationTopics.IN_CONSTRUCTION_CANDLES.value]\n            )\n        return topics\n\n    async def cancel_traded_pairs_open_orders_if_any(self) -> typing.Optional[commons_signals.SignalDependencies]:\n        dependencies = commons_signals.SignalDependencies()\n        if symbol_open_orders := [\n            order\n            for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n            if order.symbol in self.exchange_manager.exchange_config.traded_symbol_pairs\n            and not isinstance(order, trading_personal_data.MarketOrder) # market orders can't be cancelled\n        ]:\n            self.logger.info(\n                f\"Cancelling {len(symbol_open_orders)} open orders\"\n            )\n            for order in symbol_open_orders:\n                try:\n                    is_cancelled, dependency = await self.trading_mode.cancel_order(order)\n                    if is_cancelled:\n                        dependencies.extend(dependency)\n                except trading_errors.UnexpectedExchangeSideOrderStateError as err:\n                    self.logger.warning(f\"Skipped order cancel: {err}, order: {order}\")\n        return dependencies or None\n\n\nclass IndexTradingMode(trading_modes.AbstractTradingMode):\n    MODE_PRODUCER_CLASSES = [IndexTradingModeProducer]\n    MODE_CONSUMER_CLASSES = [IndexTradingModeConsumer]\n    SUPPORTS_INITIAL_PORTFOLIO_OPTIMIZATION = True\n    SUPPORTS_HEALTH_CHECK = False\n\n    def __init__(self, config, exchange_manager):\n        super().__init__(config, exchange_manager)\n        self.refresh_interval_days = 1\n        self.rebalance_trigger_min_ratio = decimal.Decimal(float(DEFAULT_REBALANCE_TRIGGER_MIN_RATIO))\n        self.rebalance_trigger_profiles: typing.Optional[list] = None\n        self.selected_rebalance_trigger_profile: typing.Optional[dict] = None\n        self.ratio_per_asset = {}\n        self.sell_unindexed_traded_coins = True\n        self.cancel_open_orders = True\n        self.total_ratio_per_asset = trading_constants.ZERO\n        self.quote_asset_rebalance_ratio_threshold = decimal.Decimal(str(DEFAULT_QUOTE_ASSET_REBALANCE_TRIGGER_MIN_RATIO))\n        self.synchronization_policy: SynchronizationPolicy = SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n        self.requires_initializing_appropriate_coins_distribution = False\n        self.indexed_coins = [] \n        self.is_processing_rebalance = False\n    \n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        trading_config = self.trading_config\n        self.refresh_interval_days = float(self.UI.user_input(\n            IndexTradingModeProducer.REFRESH_INTERVAL, commons_enums.UserInputTypes.FLOAT,\n            self.refresh_interval_days, inputs,\n            min_val=0,\n            title=\"Trigger period: Days to wait between each rebalance. Can be a fraction of a day. \"\n                  \"When set to 0, every new price will trigger a rebalance check.\",\n        ))\n        self.quote_asset_rebalance_ratio_threshold = decimal.Decimal(str(self.UI.user_input(\n            IndexTradingModeProducer.QUOTE_ASSET_REBALANCE_TRIGGER_MIN_PERCENT, commons_enums.UserInputTypes.FLOAT,\n            float(self.quote_asset_rebalance_ratio_threshold * trading_constants.ONE_HUNDRED), inputs,\n            min_val=0, max_val=100,\n            title=\"Quote asset rebalance cap: maximum allowed percent holding of traded pairs' quote asset before \"\n                \"triggering a rebalance. Useful to force a rebalance when adding quote asset to the portfolio\",\n        ))) / trading_constants.ONE_HUNDRED\n        self.rebalance_trigger_min_ratio = decimal.Decimal(str(self.UI.user_input(\n            IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT, commons_enums.UserInputTypes.FLOAT,\n            float(self.rebalance_trigger_min_ratio * trading_constants.ONE_HUNDRED), inputs,\n            min_val=0, max_val=100,\n            title=\"Rebalance cap: maximum allowed percent holding of a coin beyond initial ratios before \"\n                  \"triggering a rebalance.\",\n        ))) / trading_constants.ONE_HUNDRED\n\n        self.rebalance_trigger_profiles = self.trading_config.get(IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES, None)\n        if self.rebalance_trigger_profiles:\n            # only display selector if there are profiles to display\n            rebalance_trigger_profiles_inputs = [{\n                IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: self.UI.user_input(\n                    IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME, commons_enums.UserInputTypes.TEXT,\n                    \"profile name\", inputs,\n                    parent_input_name=IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES,\n                    array_indexes=[0],\n                    title=f\"Name: name of the reference trigger profile\"\n                ),\n                IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: self.UI.user_input(\n                    IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT, commons_enums.UserInputTypes.FLOAT,\n                    float(self.rebalance_trigger_min_ratio * trading_constants.ONE_HUNDRED), inputs,\n                    parent_input_name=IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES,\n                    array_indexes=[0],\n                    min_val=0, max_val=100,\n                    title=(\n                    \"Rebalance cap: maximum allowed percent holding of a coin beyond initial ratios before \"\n                    \"triggering a rebalance when this profile is selected.\"\n                    )\n                ),\n            }]\n            self.UI.user_input(\n                IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES, commons_enums.UserInputTypes.OBJECT_ARRAY, rebalance_trigger_profiles_inputs, inputs,\n                other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n                item_title=\"Rebalance trigger profile\",\n                title=\"Rebalance trigger profiles\",\n            )\n            selected_rebalance_trigger_profile_name = self.UI.user_input(\n                IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE, commons_enums.UserInputTypes.OPTIONS,\n                None, inputs,\n                options=[p[IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME] for p in self.rebalance_trigger_profiles],\n                title=\"Selected rebalance trigger profile, override the default Rebalance cap value.\",\n            )\n            selected_profile = [\n                p for p in self.rebalance_trigger_profiles \n                if p[IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME] == selected_rebalance_trigger_profile_name\n            ]\n            if selected_profile:\n                self.selected_rebalance_trigger_profile = selected_profile[0]\n                # apply selected rebalance trigger profile ratio\n                self.rebalance_trigger_min_ratio = decimal.Decimal(str(\n                    self.selected_rebalance_trigger_profile[IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT])\n                ) / trading_constants.ONE_HUNDRED\n            else:\n                self.logger.warning(\n                    f\"Selected rebalance trigger profile {selected_rebalance_trigger_profile_name} not found in rebalance trigger profiles: {self.rebalance_trigger_profiles}\"\n                )\n                self.selected_rebalance_trigger_profile = None\n        sync_policy: str = self.UI.user_input(\n            IndexTradingModeProducer.SYNCHRONIZATION_POLICY, commons_enums.UserInputTypes.OPTIONS,\n            self.synchronization_policy.value, inputs, \n            options=[p.value for p in SynchronizationPolicy],\n            editor_options={\"enum_titles\": [p.value.replace(\"_\", \" \") for p in SynchronizationPolicy]},\n            title=\"Synchronization policy: should coins that are removed from index be sold as soon as possible or only when rebalancing is triggered when coins don't follow the configured ratios.\",\n        )\n        try:\n            self.synchronization_policy = SynchronizationPolicy(sync_policy)\n        except ValueError as err:\n            self.logger.exception(\n                err, \n                True, \n                f\"Impossible to parse synchronization policy: {err}. Using default policy: {self.synchronization_policy.value}.\"\n            )\n        self.cancel_open_orders = float(self.UI.user_input(\n            IndexTradingModeProducer.CANCEL_OPEN_ORDERS, commons_enums.UserInputTypes.BOOLEAN,\n            self.cancel_open_orders, inputs,\n            title=\"Cancel open orders: When enabled, open orders of the index trading pairs will be canceled to free \"\n                  \"funds and invest in the index content.\",\n        ))\n        self.sell_unindexed_traded_coins = trading_config.get(\n            IndexTradingModeProducer.SELL_UNINDEXED_TRADED_COINS,\n            self.sell_unindexed_traded_coins\n        )\n        if (not self.exchange_manager or not self.exchange_manager.is_backtesting) and \\\n                authentication.Authenticator.instance().has_open_source_package():\n            self.UI.user_input(IndexTradingModeProducer.INDEX_CONTENT, commons_enums.UserInputTypes.OBJECT_ARRAY,\n                               trading_config.get(IndexTradingModeProducer.INDEX_CONTENT, None), inputs,\n                               item_title=\"Coin\",\n                               other_schema_values={\"minItems\": 0, \"uniqueItems\": True},\n                               title=\"Custom distribution: when used, only coins listed in this distribution and \"\n                                     \"in your profile traded pairs will be traded. \"\n                                     \"Leave empty to evenly allocate funds in each traded coin.\")\n            self.UI.user_input(index_distribution.DISTRIBUTION_NAME, commons_enums.UserInputTypes.TEXT,\n                               \"BTC\", inputs,\n                               other_schema_values={\"minLength\": 1},\n                               parent_input_name=IndexTradingModeProducer.INDEX_CONTENT,\n                               title=\"Name of the coin.\")\n            self.UI.user_input(index_distribution.DISTRIBUTION_VALUE, commons_enums.UserInputTypes.FLOAT,\n                               50, inputs,\n                               min_val=0,\n                               parent_input_name=IndexTradingModeProducer.INDEX_CONTENT,\n                               title=\"Weight of the coin within this distribution.\")\n        self.requires_initializing_appropriate_coins_distribution = self.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n        self.ensure_updated_coins_distribution()\n\n    @classmethod\n    def get_tentacle_config_traded_symbols(cls, config: dict, reference_market: str) -> list:\n        return [\n            symbol_util.merge_currencies(asset[index_distribution.DISTRIBUTION_NAME], reference_market)\n            for asset in (cls.get_ideal_distribution(config) or [])\n        ]\n\n    def is_updating_at_each_price_change(self):\n        return self.refresh_interval_days == 0\n\n    def automatically_update_historical_config_on_set_intervals(self) -> bool:\n        return (\n            self.supports_historical_config() \n            and self.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n        )\n\n    def ensure_updated_coins_distribution(self, adapt_to_holdings: bool = False, force_latest: bool = False):\n        distribution = self._get_supported_distribution(adapt_to_holdings, force_latest)\n        self.ratio_per_asset = {\n            asset[index_distribution.DISTRIBUTION_NAME]: asset\n            for asset in distribution\n        }\n        self.total_ratio_per_asset = decimal.Decimal(sum(\n            asset[index_distribution.DISTRIBUTION_VALUE]\n            for asset in self.ratio_per_asset.values()\n        ))\n        self.indexed_coins = self._get_filtered_traded_coins(self.ratio_per_asset)\n\n    def _get_filtered_traded_coins(self, ratio_per_asset: dict):\n        if self.exchange_manager:\n            ref_market = self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n            coins = set(\n                symbol.base\n                for symbol in self.exchange_manager.exchange_config.traded_symbols\n                if symbol.base in ratio_per_asset and symbol.quote == ref_market\n            )\n            if ref_market in ratio_per_asset and coins:\n                # there is at least 1 coin traded against ref market, can add ref market if necessary\n                coins.add(ref_market)\n            return sorted(list(coins))\n        return []\n\n    def get_coins_to_consider_for_ratio(self) -> list:\n        return self.indexed_coins + [self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market]\n\n    @classmethod\n    def get_ideal_distribution(cls, config: dict):\n        return config.get(IndexTradingModeProducer.INDEX_CONTENT, None)\n\n    @staticmethod\n    def get_default_historical_time_frame() -> typing.Optional[commons_enums.TimeFrames]:\n        return commons_enums.TimeFrames.ONE_DAY\n\n    @staticmethod\n    def use_backtesting_accurate_price_update() -> bool:\n        \"\"\"\n        Return True if the trading mode is more accurate in backtesting when using a short price update time frame\n        \"\"\"\n        # a short price update time frame is not increasing accuracy for index trading mode\n        return False\n\n    @staticmethod\n    def get_config_history_propagated_tentacles_config_keys() -> list[str]:\n        \"\"\"\n        Returns the list of config keys that should be propagated to historical configurations\n        \"\"\"\n        return [\n            # The selected rebalance trigger profile should be applied to all historical configs \n            # to ensure the user selected profile is always used\n            IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE,\n            IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES,\n            IndexTradingModeProducer.SYNCHRONIZATION_POLICY,\n        ]\n\n    def _get_currently_applied_historical_config_according_to_holdings(\n        self, config: dict, traded_bases: set[str]\n    ) -> dict:\n        # 1. check if latest config is the running one\n        if self._is_index_config_applied(config, traded_bases):\n            self.logger.info(f\"Using {self.get_name()} latest config.\")\n            return config\n        # 2. check if historical configs are available (iterating from most recent to oldest)\n        historical_configs = self.get_historical_configs(\n            0, self.exchange_manager.exchange.get_exchange_current_time()\n        )\n        if not historical_configs or (\n            # only 1 historical config which is the same as the latest config\n            len(historical_configs) == 1 and (\n                self.get_ideal_distribution(historical_configs[0]) == self.get_ideal_distribution(config)\n                and historical_configs[0][IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT] == config[IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT]\n            )\n        ):\n            # current config is the first historical config\n            self.logger.info(f\"Using {self.get_name()} latest config as no historical configs are available.\")\n            return config\n        for index, historical_config in enumerate(historical_configs):\n            if self._is_index_config_applied(historical_config, traded_bases):\n                self.logger.info(f\"Using [N-{index}] {self.get_name()} historical config distribution: {self.get_ideal_distribution(historical_config)}.\")\n                return historical_config\n        # 3. no suitable config found: return latest config\n        self.logger.info(f\"No suitable {self.get_name()} config found: using latest distribution: {self.get_ideal_distribution(config)}.\")\n        return config\n\n    def _is_index_config_applied(self, config: dict, traded_bases: set[str]) -> bool:\n        full_assets_distribution = self.get_ideal_distribution(config)\n        if not full_assets_distribution:\n            return False\n        assets_distribution = [\n            asset\n            for asset in full_assets_distribution\n            if asset[index_distribution.DISTRIBUTION_NAME] in traded_bases\n        ]\n        if len(assets_distribution) != len(full_assets_distribution):\n            # if assets are missing from traded pairs, the config is not applied\n            # might be due to delisted or renamed coins\n            missing_assets = [\n                asset[index_distribution.DISTRIBUTION_NAME]\n                for asset in full_assets_distribution\n                if asset not in assets_distribution\n            ]\n            self.logger.warning(\n                f\"Ignored {self.get_name()} config candidate as {len(missing_assets)} configured assets {missing_assets} are missing from {self.exchange_manager.exchange_name} traded pairs.\"\n            )\n            return False\n\n        total_ratio = decimal.Decimal(sum(\n            asset[index_distribution.DISTRIBUTION_VALUE]\n            for asset in assets_distribution\n        ))\n        if total_ratio == trading_constants.ZERO:\n            return False\n        min_trigger_ratio = self._get_config_min_ratio(config)\n        for asset_distrib in assets_distribution:\n            target_ratio = decimal.Decimal(str(asset_distrib[index_distribution.DISTRIBUTION_VALUE])) / total_ratio\n            coin_ratio = self.exchange_manager.exchange_personal_data.portfolio_manager. \\\n                portfolio_value_holder.get_holdings_ratio(\n                    asset_distrib[index_distribution.DISTRIBUTION_NAME], traded_symbols_only=True\n                )\n            if not (target_ratio - min_trigger_ratio <= coin_ratio <= target_ratio + min_trigger_ratio):\n                # not enough or too much in portfolio\n                return False\n        return True\n\n    def _get_config_min_ratio(self, config: dict) -> decimal.Decimal:\n        ratio = None\n        rebalance_trigger_profiles = config.get(IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES, None)\n        if rebalance_trigger_profiles:\n            # 1. try to get ratio from selected rebalance trigger profile\n            selected_rebalance_trigger_profile_name =config.get(IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE, None)\n            selected_profile = [\n                p for p in rebalance_trigger_profiles \n                if p[IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME] == selected_rebalance_trigger_profile_name\n            ]\n            if selected_profile:\n                selected_rebalance_trigger_profile = selected_profile[0]\n                ratio = selected_rebalance_trigger_profile[IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT]\n        if ratio is None:\n            # 2. try to get ratio from direct config\n            ratio = config.get(IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT)\n        if ratio is None:\n            # 3. default to current config ratio\n            return self.rebalance_trigger_min_ratio\n        return decimal.Decimal(str(ratio)) / trading_constants.ONE_HUNDRED\n\n    def _get_supported_distribution(self, adapt_to_holdings: bool, force_latest: bool) -> list:\n        if detailed_distribution := self.get_ideal_distribution(self.trading_config):\n            traded_bases = set(\n                symbol.base\n                for symbol in self.exchange_manager.exchange_config.traded_symbols\n            )\n            traded_bases.add(self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market)\n            if (\n                (adapt_to_holdings or force_latest) \n                and self.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n            ):\n                if adapt_to_holdings:\n                    # when policy is SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE, the latest config might not be the \n                    # running one: confirm this using historical configs\n                    index_config = self._get_currently_applied_historical_config_according_to_holdings(\n                        self.trading_config, traded_bases\n                    )\n                else:\n                    # force latest available config\n                    try:\n                        index_config = self.get_historical_configs(\n                            0, self.exchange_manager.exchange.get_exchange_current_time()\n                        )[0]\n                        self.logger.info(f\"Updated {self.get_name()} to use latest distribution: {self.get_ideal_distribution(index_config)}.\")\n                    except IndexError:\n                        index_config = self.trading_config\n                detailed_distribution = self.get_ideal_distribution(index_config)\n                if not detailed_distribution:\n                    raise ValueError(f\"No distribution found in historical index config: {index_config}\")\n            distribution = [\n                asset\n                for asset in detailed_distribution\n                if asset[index_distribution.DISTRIBUTION_NAME] in traded_bases\n            ]\n            if removed_assets := [\n                asset[index_distribution.DISTRIBUTION_NAME]\n                for asset in detailed_distribution\n                if asset not in distribution\n            ]:\n                self.logger.info(\n                    f\"Ignored {len(removed_assets)} assets {removed_assets} from configured \"\n                    f\"distribution as absent from traded pairs.\"\n                )\n            return distribution\n        else:\n            # compute uniform distribution over traded assets\n            return index_distribution.get_uniform_distribution([\n                symbol.base\n                for symbol in self.exchange_manager.exchange_config.traded_symbols\n            ]) if self.exchange_manager else []\n\n    def get_removed_coins_from_config(self, available_traded_bases) -> list:\n        removed_coins = []\n        if self.get_ideal_distribution(self.trading_config) and self.sell_unindexed_traded_coins:\n            # only remove non indexed coins if an ideal distribution is set\n            removed_coins = [\n                coin\n                for coin in available_traded_bases\n                if coin not in self.indexed_coins\n                and coin != self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n            ]\n        if self.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE:\n            # identify coins to sell from previous config\n            if not (self.previous_trading_config and self.trading_config):\n                return removed_coins\n            current_coins = [\n                asset[index_distribution.DISTRIBUTION_NAME]\n                for asset in (self.get_ideal_distribution(self.trading_config) or [])\n            ]\n            ref_market = self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n            return list(set(removed_coins + [\n                asset[index_distribution.DISTRIBUTION_NAME]\n                for asset in self.previous_trading_config[IndexTradingModeProducer.INDEX_CONTENT]\n                if asset[index_distribution.DISTRIBUTION_NAME] not in current_coins\n                    and (\n                        asset[index_distribution.DISTRIBUTION_NAME]\n                        != self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n                    )\n            ]))\n        elif self.synchronization_policy == SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE:\n            # identify coins to sell from historical configs\n            historical_configs = self.get_historical_configs(\n                # use 0 a the initial config time as only relevant historical configs should be available\n                0, self.exchange_manager.exchange.get_exchange_current_time()\n            )\n            if not (historical_configs and self.trading_config):\n                return removed_coins\n            current_coins = [\n                asset[index_distribution.DISTRIBUTION_NAME]\n                for asset in (self.get_ideal_distribution(self.trading_config) or [])\n            ]\n            ref_market = self.exchange_manager.exchange_personal_data.portfolio_manager.reference_market\n            removed_coins_from_historical_configs = set()\n            for historical_config in historical_configs:\n                for asset in historical_config[IndexTradingModeProducer.INDEX_CONTENT]:\n                    asset_name = asset[index_distribution.DISTRIBUTION_NAME]\n                    if asset_name not in current_coins and asset_name != ref_market:\n                        removed_coins_from_historical_configs.add(asset_name)\n            return list(removed_coins_from_historical_configs.union(removed_coins))\n        else:\n            self.logger.error(f\"Unknown synchronization policy: {self.synchronization_policy}\")\n            return []\n\n    def get_target_ratio(self, currency) -> decimal.Decimal:\n        if currency in self.ratio_per_asset:\n            try:\n                return (\n                    decimal.Decimal(str(\n                        self.ratio_per_asset[currency][index_distribution.DISTRIBUTION_VALUE]\n                    )) / self.total_ratio_per_asset\n                )\n            except (decimal.DivisionByZero, decimal.InvalidOperation):\n                pass\n        return trading_constants.ZERO\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return True\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n        ]\n\n    def get_current_state(self) -> tuple:\n        return trading_enums.EvaluatorStates.NEUTRAL.name, f\"Indexing {len(self.indexed_coins)} coins\"\n\n    async def single_exchange_process_optimize_initial_portfolio(\n        self, sellable_assets: list, target_asset: str, tickers: dict\n    ) -> list:\n        return await trading_modes.convert_assets_to_target_asset(\n            self, sellable_assets, target_asset, tickers\n        )\n"
  },
  {
    "path": "Trading/Mode/index_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"IndexTradingMode\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Mode/index_trading_mode/resources/IndexTradingMode.md",
    "content": "The Index trading mode splits and maintains your portfolio distributed between the traded currencies. It enables \nto maintain a crypto index based on your choice of coins.\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/index-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=IndexTradingModeDocs\">\nfull Index trading mode guide</a>.\n\n### Content of the Index\nThe Index is defined by the selected traded pairs against your reference market in the \nprofile configuration section.  \nExample:\n- Your reference market is USDT\n- Your traded pairs are BTC/USDT, ETH/USDT, SOL/USDT, ADA/USDT\nThen your index will be made of 25% BTC, 25% ETH, 25% SOL and 25% ADA. Each coin's holding % will be computed \nagainst USDT and checked on a regular basis. You can also specify a specific % for each coin using a Custom \ndistribution using the [Premium OctoBot extension](extensions).\n\nWhen starting the Index trading mode with a new configuration, or if your current portfolio doesn't reflect\nthe target of the index, your portfolio will automatically be adapted to reproduce the index at the best\naccuracy possible.\n\n### Index rebalance\nAn Index rebalance is the event when OctoBot is sending orders to the exchange to adapt the content of\nyour portfolio in order to reproduce the configuration of your Index.  \nOnce your Index trading mode has started, OctoBot will maintain the index content by \nautomatically checking the content of your portfolio of a regular basis and will trigger a rebalance\nif necessary.\n\nYour portfolio content is checked every configured `Trigger period` days. Decimal values can be used to check multiple \ntimes a day. If during an index check, \nyour OctoBot detects that your portfolio content doesn't comply with your index configuration, it will\ntrigger a rebalance.\n\nIf `Trigger period` is set to `0`, then each new price of any indexed coin will trigger an index checkup and a rebalance\nif conditions are met.\n\n### Rebalance cap\nWhen checking for rebalance, the Index trading mode also uses your `Rebalance cap` configuration before\nconsidering your portfolio out of synch with your index configuration.\nThe Rebalance cap is an allowed percent of allocation that will avoid triggering a rebalance as long as any\ncoin holding is still within the ideal holding % plus or minus the rebalance cap.  \nExample:\nAn index on 3 coins with a 33.33% target on each coin and a Rebalance cap of 5% will trigger a rebalance if \nthe holding if any of those 3 coins takes more than 38.33% or less than 28.33% of the portfolio\n\nWarning on high Rebalance caps: When your index Rebalance cap is higher or equal to the target holding % of a coin, no rebalance \nwill be triggered if your holdings of this coin become very low, rebalances will only be triggered when holdings are \ngetting too high. This is a special case that can happen when using a large Rebalance cap.\nExample:  \nLet's take an index on 10 coins using a 10% target for each coin. Using a Rebalance cap of 11% will only trigger a \nrebalance if any of those 10 coins take more than 21% of the portfolio (10% + 11%). The other side: 10% - 11% = -1% \nis negative and therefore can't happen, which means rebalances won't be triggered from lower holdings in this\nconfiguration. Using a 9% rebalance cap however would trigger a rebalance at 1% holdings (10% - 9%). \n\nPlease note that if the % held of a coin is 0%, then a rebalance will always trigger, ignoring Rebalance cap.\n\n### Minimum funds\nTo use the Index Trading Mode, the minimum required funds are twice the minimum exchange order amount for every \ntraded coin. This means that when trading 3 coins on Binance, at least 3 times $5 x2, which is $30 is required.  \nPlease note that this is the bare minimum, it's better to have at least twice this amount. If the minimum is reached, \nthe Index Trading Mode will stop updating its portfolio according to the index until the value of the portfolio \nraises back above the required minimum.\n\n### OctoBot cloud indexes\nThe [Premium OctoBot extension](extensions) enables your open source OctoBot to use and customize OctoBot cloud's\n<a target=\"_blank\" rel=\"noopener\" href=\"https://app.octobot.cloud/explore?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=IndexTradingModeDocs\">automatically configured indexes</a>. \n"
  },
  {
    "path": "Trading/Mode/index_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/index_trading_mode/tests/test_index_distribution.py",
    "content": "import decimal\nimport pytest\n\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\n\n\ndef test_get_uniform_distribution():\n    assert index_distribution.get_uniform_distribution([]) == []\n    assert index_distribution.get_uniform_distribution([\"BTC\", \"1\", \"2\", \"3\"]) == [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 25,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"1\",\n            index_distribution.DISTRIBUTION_VALUE: 25,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"2\",\n            index_distribution.DISTRIBUTION_VALUE: 25,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"3\",\n            index_distribution.DISTRIBUTION_VALUE: 25,\n        }\n    ]\n    assert index_distribution.get_uniform_distribution([\"BTC\", \"1\", \"2\"]) == [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 33.3,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"1\",\n            index_distribution.DISTRIBUTION_VALUE: 33.3,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"2\",\n            index_distribution.DISTRIBUTION_VALUE: 33.3,\n        },\n    ]\n\n\ndef test_get_linear_distribution():\n    with pytest.raises(ValueError):\n        index_distribution.get_linear_distribution({})\n    assert index_distribution.get_linear_distribution({\n        \"BTC\": decimal.Decimal(122),\n        \"1\": decimal.Decimal(12),\n        \"2\": decimal.Decimal(\"0.4\"),\n        \"3\": decimal.Decimal(44)\n    }) == [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 68.4,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"1\",\n            index_distribution.DISTRIBUTION_VALUE: 6.7,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"2\",\n            index_distribution.DISTRIBUTION_VALUE: 0.2,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"3\",\n            index_distribution.DISTRIBUTION_VALUE: 24.7,\n        }\n    ]\n    assert index_distribution.get_linear_distribution({\n        \"BTC\": decimal.Decimal(12332),\n        \"1\": decimal.Decimal(12),\n        \"3\": decimal.Decimal(433334)\n    }) == [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 2.8,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"1\",\n            index_distribution.DISTRIBUTION_VALUE: 0,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"3\",\n            index_distribution.DISTRIBUTION_VALUE: 97.2,\n        },\n    ]\n\n\ndef test_get_smoothed_distribution():\n    with pytest.raises(ValueError):\n        index_distribution.get_smoothed_distribution({})\n    assert index_distribution.get_smoothed_distribution({\n        \"BTC\": decimal.Decimal(122),\n        \"1\": decimal.Decimal(12),\n        \"2\": decimal.Decimal(\"0.4\"),\n        \"3\": decimal.Decimal(44)\n    }) == [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 43.1,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"1\",\n            index_distribution.DISTRIBUTION_VALUE: 19.9,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"2\",\n            index_distribution.DISTRIBUTION_VALUE: 6.4,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"3\",\n            index_distribution.DISTRIBUTION_VALUE: 30.7,\n        }\n    ]\n    assert index_distribution.get_smoothed_distribution({\n        \"BTC\": decimal.Decimal(12332),\n        \"1\": decimal.Decimal(12),\n        \"3\": decimal.Decimal(433334)\n    }) == [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 22.9,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"1\",\n            index_distribution.DISTRIBUTION_VALUE: 2.3,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"3\",\n            index_distribution.DISTRIBUTION_VALUE: 74.9,\n        },\n    ]\n"
  },
  {
    "path": "Trading/Mode/index_trading_mode/tests/test_index_trading_mode.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport time\nimport pytest\nimport pytest_asyncio\nimport os.path\nimport mock\nimport decimal\n\nimport async_channel.util as channel_util\n\nimport octobot_commons.enums as commons_enum\nimport octobot_commons.tests.test_config as test_config\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.configuration as commons_configuration\nimport octobot_commons.signals as commons_signals\n\nimport octobot_backtesting.api as backtesting_api\n\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.modes\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.signals as trading_signals\n\nimport tentacles.Trading.Mode as Mode\nimport tentacles.Trading.Mode.index_trading_mode.index_trading as index_trading\nimport tentacles.Trading.Mode.index_trading_mode.index_distribution as index_distribution\n\nimport tests.test_utils.memory_check_util as memory_check_util\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def tools():\n    trader = None\n    try:\n        tentacles_manager_api.reload_tentacle_info()\n        mode, trader = await _get_tools()\n        yield mode, trader\n    finally:\n        if trader:\n            await _stop(trader.exchange_manager)\n\n\nasync def test_run_independent_backtestings_with_memory_check():\n    \"\"\"\n    Should always be called first here to avoid other tests' related memory check issues\n    \"\"\"\n    tentacles_setup_config = tentacles_manager_api.create_tentacles_setup_config_with_tentacles(\n        Mode.IndexTradingMode,\n    )\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_TIME_FRAME] = [commons_enum.TimeFrames.FOUR_HOURS]\n\n    _CONFIG = {\n        Mode.IndexTradingMode.get_name(): {\n            \"required_strategies\": [],\n            \"refresh_interval\": 7,\n            \"rebalance_trigger_min_percent\": 5,\n            \"index_content\": []\n        },\n    }\n\n    def config_proxy(tentacles_setup_config, klass):\n        try:\n            return _CONFIG[klass if isinstance(klass, str) else klass.get_name()]\n        except KeyError:\n            return {}\n\n    with tentacles_manager_api.local_tentacle_config_proxy(config_proxy):\n        with mock.patch.object(octobot_trading.modes.AbstractTradingMode, \"get_historical_config\", mock.Mock()) \\\n            as get_historical_config:\n            await memory_check_util.run_independent_backtestings_with_memory_check(\n                config, tentacles_setup_config, use_multiple_asset_data_file=True\n            )\n            # should not be called when no historical config is available (or it will log errors)\n            get_historical_config.assert_not_called()\n\n\ndef _get_config(tools, update):\n    mode, trader = tools\n    config = tentacles_manager_api.get_tentacle_config(trader.exchange_manager.tentacles_setup_config, mode.__class__)\n    return {**config, **update}\n\n\nasync def test_init_default_values(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    assert mode.refresh_interval_days == 1\n    assert mode.rebalance_trigger_min_ratio == decimal.Decimal(str(index_trading.DEFAULT_REBALANCE_TRIGGER_MIN_RATIO))\n    assert mode.quote_asset_rebalance_ratio_threshold == decimal.Decimal(str(index_trading.DEFAULT_QUOTE_ASSET_REBALANCE_TRIGGER_MIN_RATIO))\n    assert mode.ratio_per_asset == {'BTC': {'name': 'BTC', 'value': decimal.Decimal(100)}}\n    assert mode.total_ratio_per_asset == decimal.Decimal(100)\n    assert mode.synchronization_policy == index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n    assert mode.requires_initializing_appropriate_coins_distribution is False\n    assert mode.indexed_coins == [\"BTC\"]\n    assert mode.selected_rebalance_trigger_profile is None\n    assert mode.rebalance_trigger_profiles is None\n\n\nasync def test_init_config_values(tools):\n    update = {\n        index_trading.IndexTradingModeProducer.REFRESH_INTERVAL: 72,\n        index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY: index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE.value,\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 10.2,\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: None,\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.2,\n            },\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-2\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20.2,\n            },\n        ],\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_distribution.DISTRIBUTION_VALUE: 53,\n            },\n            {\n                index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_distribution.DISTRIBUTION_VALUE: 1,\n            },\n            {\n                index_distribution.DISTRIBUTION_NAME: \"SOL\",\n                index_distribution.DISTRIBUTION_VALUE: 1,\n            },\n        ]\n    }\n    # no selected rebalance trigger profile\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    assert mode.refresh_interval_days == 72\n    assert mode.rebalance_trigger_min_ratio == decimal.Decimal(\"0.102\")\n    assert mode.selected_rebalance_trigger_profile is None\n    assert mode.rebalance_trigger_profiles ==  [\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.2,\n        },\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-2\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20.2,\n        },\n    ]\n    assert mode.synchronization_policy == index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    assert mode.requires_initializing_appropriate_coins_distribution is True\n    assert mode.ratio_per_asset == {\n        \"BTC\": {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 1,\n        },\n    }\n    assert mode.total_ratio_per_asset == decimal.Decimal(\"1\")\n    assert mode.indexed_coins == [\"BTC\"]\n\n    # now with ETH as traded assets\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"ADA/USDT\", \"BTC/USDT\"]\n    ]\n    mode.trading_config[index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE] = \"profile-1\"\n    mode.init_user_inputs({})\n    assert mode.refresh_interval_days == 72\n    assert mode.rebalance_trigger_profiles ==  [\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.2,\n        },\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-2\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20.2,\n        },\n    ]\n    assert mode.selected_rebalance_trigger_profile == {\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.2,\n    }   # applied profile\n    assert mode.rebalance_trigger_min_ratio == decimal.Decimal(\"0.052\")\n    assert mode.ratio_per_asset == {\n        \"ETH\": {\n            index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_distribution.DISTRIBUTION_VALUE: 53,\n        },\n        \"BTC\": {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 1,\n        }\n        # SOL is not added\n    }\n    assert mode.total_ratio_per_asset == decimal.Decimal(\"54\")\n    assert mode.indexed_coins == [\"BTC\", \"ETH\"]  # sorted list\n\n    # refresh user inputs\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"ADA/USDT\", \"BTC/USDT\", \"SOL/USDT\"]\n    ]\n    mode.init_user_inputs({})\n    assert mode.refresh_interval_days == 72\n    assert mode.rebalance_trigger_min_ratio == decimal.Decimal(\"0.052\")\n    assert mode.ratio_per_asset == {\n        \"ETH\": {\n            index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_distribution.DISTRIBUTION_VALUE: 53,\n        },\n        \"BTC\": {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 1,\n        },\n        \"SOL\": {\n            index_distribution.DISTRIBUTION_NAME: \"SOL\",\n            index_distribution.DISTRIBUTION_VALUE: 1,\n        },\n    }\n    assert mode.total_ratio_per_asset == decimal.Decimal(\"55\")\n    assert mode.indexed_coins == [\"BTC\", \"ETH\", \"SOL\"]  # sorted list\n\n    # add ref market in coin rations\n    mode.trading_config[\"index_content\"] = [\n        {\n            index_distribution.DISTRIBUTION_NAME: \"USDT\",\n            index_distribution.DISTRIBUTION_VALUE: 75,\n        },\n        {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 25,\n        },\n    ]\n    # select profile 2\n    mode.trading_config[index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE] = \"profile-2\"\n    mode.init_user_inputs({})\n    assert mode.refresh_interval_days == 72\n    assert mode.selected_rebalance_trigger_profile == {\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-2\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20.2,\n    }   # applied profile\n    assert mode.rebalance_trigger_min_ratio == decimal.Decimal(\"0.202\")\n    assert mode.ratio_per_asset == {\n        \"BTC\": {\n            index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_distribution.DISTRIBUTION_VALUE: 25,\n        },\n        \"USDT\": {\n            index_distribution.DISTRIBUTION_NAME: \"USDT\",\n            index_distribution.DISTRIBUTION_VALUE: 75,\n        },\n    }\n    assert mode.total_ratio_per_asset == decimal.Decimal(\"100\")\n    assert mode.indexed_coins == [\"BTC\", \"USDT\"]  # sorted list\n\n    # unknown profile\n    mode.trading_config[index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE] = \"unknown\"\n    mode.init_user_inputs({})\n    # back to non-profile config values bu profiles are loaded\n    assert mode.rebalance_trigger_profiles ==  [\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 5.2,\n        },\n        {\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-2\",\n            index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 20.2,\n        },\n    ]\n    assert mode.selected_rebalance_trigger_profile is None\n    assert mode.rebalance_trigger_min_ratio == decimal.Decimal(str(10.2 / 100))\n\n    # invalid synchronization policy\n    mode.trading_config[index_trading.IndexTradingModeProducer.SYNCHRONIZATION_POLICY] = \"invalid_policy\"\n    mode.init_user_inputs({})   # does no raise error\n    # use current or default value\n    assert mode.synchronization_policy == index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n\n\nasync def test_single_exchange_process_optimize_initial_portfolio(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n\n    with mock.patch.object(\n            octobot_trading.modes, \"convert_assets_to_target_asset\", mock.AsyncMock(return_value=[\"order_1\"])\n    ) as convert_assets_to_target_asset_mock, mock.patch.object(\n        mode, \"cancel_order\", mock.AsyncMock()\n    ) as cancel_order_mock:\n        # no open order\n        orders = await mode.single_exchange_process_optimize_initial_portfolio([\"BTC\", \"ETH\"], \"USDT\", {})\n        convert_assets_to_target_asset_mock.assert_called_once_with(mode, [\"BTC\", \"ETH\"], \"USDT\", {})\n        cancel_order_mock.assert_not_called()\n        assert orders == [\"order_1\"]\n        convert_assets_to_target_asset_mock.reset_mock()\n\n        # open orders of the given symbol are cancelled\n        open_order_1 = trading_personal_data.SellLimitOrder(trader)\n        open_order_2 = trading_personal_data.BuyLimitOrder(trader)\n        open_order_3 = trading_personal_data.BuyLimitOrder(trader)\n        open_order_1.update(order_type=trading_enums.TraderOrderType.SELL_LIMIT,\n                            order_id=\"open_order_1_id\",\n                            symbol=\"BTC/USDT\",\n                            current_price=decimal.Decimal(\"70\"),\n                            quantity=decimal.Decimal(\"10\"),\n                            price=decimal.Decimal(\"70\"))\n        open_order_2.update(order_type=trading_enums.TraderOrderType.BUY_LIMIT,\n                            order_id=\"open_order_2_id\",\n                            symbol=\"ETH/USDT\",\n                            current_price=decimal.Decimal(\"70\"),\n                            quantity=decimal.Decimal(\"10\"),\n                            price=decimal.Decimal(\"70\"),\n                            reduce_only=True)\n        open_order_3.update(order_type=trading_enums.TraderOrderType.BUY_LIMIT,\n                            order_id=\"open_order_2_id\",\n                            symbol=\"ADA/USDT\",\n                            current_price=decimal.Decimal(\"70\"),\n                            quantity=decimal.Decimal(\"10\"),\n                            price=decimal.Decimal(\"70\"),\n                            reduce_only=True)\n        await mode.exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(open_order_1)\n        await mode.exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(open_order_2)\n        await mode.exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(open_order_3)\n        mode.exchange_manager.exchange_config.traded_symbol_pairs = [\"BTC/USDT\", \"ETH/USDT\"]\n\n        orders = await mode.single_exchange_process_optimize_initial_portfolio([\"BTC\", \"ETH\"], \"USDT\", {})\n        convert_assets_to_target_asset_mock.assert_called_once_with(mode, [\"BTC\", \"ETH\"], \"USDT\", {})\n        cancel_order_mock.assert_not_called()\n        assert orders == [\"order_1\"]\n        convert_assets_to_target_asset_mock.reset_mock()\n\n\nasync def test_get_target_ratio_with_config(tools):\n    update = {\n        \"refresh_interval\": 72,\n        \"rebalance_trigger_min_percent\": 10.2,\n        \"index_content\": [\n            {\n                index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_distribution.DISTRIBUTION_VALUE: 1,\n            },\n            {\n                index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_distribution.DISTRIBUTION_VALUE: 53,\n            },\n        ]\n    }\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    assert mode.get_target_ratio(\"ETH\") == decimal.Decimal('0')\n    assert mode.get_target_ratio(\"BTC\") == decimal.Decimal(\"1\")  # use 100% BTC as others are not in traded pairs\n    assert mode.get_target_ratio(\"SOL\") == decimal.Decimal(\"0\")\n\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"ADA/USDT\", \"BTC/USDT\", \"SOL/USDT\"]\n    ]\n    mode.init_user_inputs({})\n    assert mode.get_target_ratio(\"ETH\") == decimal.Decimal('0.9814814814814814814814814815')\n    assert mode.get_target_ratio(\"BTC\") == decimal.Decimal(\"0.01851851851851851851851851852\")\n    assert mode.get_target_ratio(\"SOL\") == decimal.Decimal(\"0\")\n\n\nasync def test_get_target_ratio_without_config(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    assert mode.get_target_ratio(\"ETH\") == decimal.Decimal('0')\n    assert mode.get_target_ratio(\"BTC\") == decimal.Decimal(\"1\")\n    assert mode.get_target_ratio(\"SOL\") == decimal.Decimal(\"0\")\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"SOL/USDT\", \"BTC/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    assert mode.get_target_ratio(\"ETH\") == decimal.Decimal('0.3333333333333333617834929233')\n    assert mode.get_target_ratio(\"BTC\") == decimal.Decimal(\"0.3333333333333333617834929233\")\n    assert mode.get_target_ratio(\"SOL\") == decimal.Decimal(\"0.3333333333333333617834929233\")\n    assert mode.get_target_ratio(\"ADA\") == decimal.Decimal(\"0\")\n\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"BTC/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    assert mode.get_target_ratio(\"ETH\") == decimal.Decimal('0.5')\n    assert mode.get_target_ratio(\"BTC\") == decimal.Decimal(\"0.5\")\n    assert mode.get_target_ratio(\"SOL\") == decimal.Decimal(\"0\")\n\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"BTC/USDT\", \"ADA/USDT\", \"SOL/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    assert mode.get_target_ratio(\"ETH\") == decimal.Decimal('0.25')\n    assert mode.get_target_ratio(\"BTC\") == decimal.Decimal(\"0.25\")\n    assert mode.get_target_ratio(\"SOL\") == decimal.Decimal(\"0.25\")\n\n\nasync def test_ohlcv_callback(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    current_time = time.time()\n    with mock.patch.object(producer, \"ensure_index\", mock.AsyncMock()) as ensure_index_mock, \\\n        mock.patch.object(producer, \"_notify_if_missing_too_many_coins\", mock.Mock()) \\\n            as _notify_if_missing_too_many_coins_mock:\n        with mock.patch.object(\n                trader.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=current_time)\n        ) as get_exchange_current_time_mock:\n            # not enough indexed coins\n            mode.indexed_coins = []\n            assert producer._last_trigger_time == 0\n            await producer.ohlcv_callback(\"binance\", \"123\", \"BTC\", \"BTC/USDT\", None, None)\n            ensure_index_mock.assert_not_called()\n            _notify_if_missing_too_many_coins_mock.assert_not_called()\n            assert get_exchange_current_time_mock.call_count == 1   # only called once as no historical config exists\n            get_exchange_current_time_mock.reset_mock()\n            assert producer._last_trigger_time == current_time\n\n            # enough coins\n            mode.indexed_coins = [1, 2, 3]\n            # already called on this time\n            await producer.ohlcv_callback(\"binance\", \"123\", \"BTC\", \"BTC/USDT\", None, None)\n            ensure_index_mock.assert_not_called()\n            _notify_if_missing_too_many_coins_mock.assert_not_called()\n            assert get_exchange_current_time_mock.call_count == 1\n\n            assert producer._last_trigger_time == current_time\n        with mock.patch.object(\n                trader.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=current_time * 2)\n        ) as get_exchange_current_time_mock:\n            mode.indexed_coins = [1, 2, 3]\n            await producer.ohlcv_callback(\"binance\", \"123\", \"BTC\", \"BTC/USDT\", None, None)\n            ensure_index_mock.assert_called_once()\n            _notify_if_missing_too_many_coins_mock.assert_called_once()\n            assert get_exchange_current_time_mock.call_count == 1\n            assert producer._last_trigger_time == current_time * 2\n\n\nasync def test_notify_if_missing_too_many_coins(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(producer.logger, \"error\", mock.Mock()) as error_mock:\n        mode.trading_config[producer.INDEX_CONTENT] = [1, 2, 3, 4, 5]\n        mode.indexed_coins = [1, 2, 3, 4, 5]\n        producer._notify_if_missing_too_many_coins()\n        error_mock.assert_not_called()\n\n        mode.indexed_coins = [1, 2, 3]\n        producer._notify_if_missing_too_many_coins()\n        error_mock.assert_not_called()\n\n        # error\n        mode.indexed_coins = [1, 2]\n        producer._notify_if_missing_too_many_coins()\n        error_mock.assert_called_once()\n        error_mock.reset_mock()\n\n        # error\n        mode.indexed_coins = []\n        producer._notify_if_missing_too_many_coins()\n        error_mock.assert_called_once()\n        error_mock.reset_mock()\n\n\nasync def test_ensure_index(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n    with mock.patch.object(\n            producer, \"_wait_for_symbol_prices_and_profitability_init\", mock.AsyncMock()\n    ) as _wait_for_symbol_prices_and_profitability_init_mock, \\\n        mock.patch.object(producer, \"cancel_traded_pairs_open_orders_if_any\", mock.AsyncMock(return_value=dependencies)) \\\n            as _cancel_traded_pairs_open_orders_if_any:\n        with mock.patch.object(producer, \"_trigger_rebalance\", mock.AsyncMock()) as _trigger_rebalance_mock:\n            with mock.patch.object(\n                    producer, \"_get_rebalance_details\", mock.Mock(return_value=(False, {}))\n            ) as _get_rebalance_details_mock:\n                await producer.ensure_index()\n                assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n                    index_trading.IndexActivity.REBALANCING_SKIPPED\n                )\n                _cancel_traded_pairs_open_orders_if_any.assert_called_once()\n                _cancel_traded_pairs_open_orders_if_any.reset_mock()\n                _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once()\n                _wait_for_symbol_prices_and_profitability_init_mock.reset_mock()\n                _get_rebalance_details_mock.assert_called_once()\n                _trigger_rebalance_mock.assert_not_called()\n            with mock.patch.object(\n                    producer, \"_get_rebalance_details\", mock.Mock(return_value=(True, {\"plop\": 1}))\n            ) as _get_rebalance_details_mock:\n                await producer.ensure_index()\n                assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n                    index_trading.IndexActivity.REBALANCING_DONE, {\"plop\": 1}\n                )\n                _cancel_traded_pairs_open_orders_if_any.assert_called_once()\n                _cancel_traded_pairs_open_orders_if_any.reset_mock()\n                _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once()\n                _wait_for_symbol_prices_and_profitability_init_mock.reset_mock()\n                _get_rebalance_details_mock.assert_called_once()\n                _trigger_rebalance_mock.assert_called_once_with({\"plop\": 1}, dependencies)\n                _trigger_rebalance_mock.reset_mock()\n            with mock.patch.object(\n                    producer, \"_get_rebalance_details\", mock.Mock(return_value=(True, {\"plop\": 1}))\n            ) as _get_rebalance_details_mock:\n                producer.trading_mode.cancel_open_orders = False\n                await producer.ensure_index()\n                assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n                    index_trading.IndexActivity.REBALANCING_DONE, {\"plop\": 1}\n                )\n                _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once()\n                _wait_for_symbol_prices_and_profitability_init_mock.reset_mock()\n                _get_rebalance_details_mock.assert_called_once()\n                _cancel_traded_pairs_open_orders_if_any.assert_not_called()\n                _trigger_rebalance_mock.assert_called_once_with({\"plop\": 1}, None)\n\n        # Test with requires_initializing_appropriate_coins_distribution = True\n        with mock.patch.object(producer, \"_trigger_rebalance\", mock.AsyncMock()) as _trigger_rebalance_mock:\n            with mock.patch.object(\n                    producer, \"_get_rebalance_details\", mock.Mock(return_value=(False, {}))\n            ) as _get_rebalance_details_mock:\n                with mock.patch.object(\n                        mode, \"ensure_updated_coins_distribution\", mock.Mock()\n                ) as ensure_updated_coins_distribution_mock:\n                    # Set the flag to True\n                    mode.requires_initializing_appropriate_coins_distribution = True\n                    producer.trading_mode.cancel_open_orders = True\n                    await producer.ensure_index()\n                    # Verify ensure_updated_coins_distribution was called with adapt_to_holdings=True\n                    ensure_updated_coins_distribution_mock.assert_called_once_with(adapt_to_holdings=True)\n                    # Verify the flag was set to False\n                    assert mode.requires_initializing_appropriate_coins_distribution is False\n                    assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n                        index_trading.IndexActivity.REBALANCING_SKIPPED\n                    )\n                    _cancel_traded_pairs_open_orders_if_any.assert_called_once()\n                    _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once()\n                    _get_rebalance_details_mock.assert_called_once()\n                    _trigger_rebalance_mock.assert_not_called()\n                    ensure_updated_coins_distribution_mock.reset_mock()\n                    _cancel_traded_pairs_open_orders_if_any.reset_mock()\n                    _wait_for_symbol_prices_and_profitability_init_mock.reset_mock()\n                    _get_rebalance_details_mock.reset_mock()\n\n            with mock.patch.object(\n                    producer, \"_get_rebalance_details\", mock.Mock(return_value=(True, {\"plop\": 1}))\n            ) as _get_rebalance_details_mock:\n                with mock.patch.object(\n                        mode, \"ensure_updated_coins_distribution\", mock.Mock()\n                ) as ensure_updated_coins_distribution_mock:\n                    # Set the flag to True and disable cancel_open_orders\n                    mode.requires_initializing_appropriate_coins_distribution = True\n                    producer.trading_mode.cancel_open_orders = False\n                    await producer.ensure_index()\n                    # Verify ensure_updated_coins_distribution was called with adapt_to_holdings=True\n                    ensure_updated_coins_distribution_mock.assert_called_once_with(adapt_to_holdings=True)\n                    # Verify the flag was set to False\n                    assert mode.requires_initializing_appropriate_coins_distribution is False\n                    assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n                        index_trading.IndexActivity.REBALANCING_DONE, {\"plop\": 1}\n                    )\n                    _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once()\n                    _get_rebalance_details_mock.assert_called_once()\n                    _cancel_traded_pairs_open_orders_if_any.assert_not_called()\n                    _trigger_rebalance_mock.assert_called_once_with({\"plop\": 1}, None)\n                    ensure_updated_coins_distribution_mock.reset_mock()\n                    _wait_for_symbol_prices_and_profitability_init_mock.reset_mock()\n                    _get_rebalance_details_mock.reset_mock()\n                    _trigger_rebalance_mock.reset_mock()\n\n        # Test with requires_initializing_appropriate_coins_distribution = False (default)\n        with mock.patch.object(producer, \"_trigger_rebalance\", mock.AsyncMock()) as _trigger_rebalance_mock:\n            with mock.patch.object(\n                    producer, \"_get_rebalance_details\", mock.Mock(return_value=(False, {}))\n            ) as _get_rebalance_details_mock:\n                with mock.patch.object(\n                        mode, \"ensure_updated_coins_distribution\", mock.Mock()\n                ) as ensure_updated_coins_distribution_mock:\n                    # Ensure the flag is False (default state)\n                    mode.requires_initializing_appropriate_coins_distribution = False\n                    producer.trading_mode.cancel_open_orders = True\n                    await producer.ensure_index()\n                    # Verify ensure_updated_coins_distribution was NOT called\n                    ensure_updated_coins_distribution_mock.assert_not_called()\n                    # Verify the flag remains False\n                    assert mode.requires_initializing_appropriate_coins_distribution is False\n                    assert producer.last_activity == octobot_trading.modes.TradingModeActivity(\n                        index_trading.IndexActivity.REBALANCING_SKIPPED\n                    )\n                    _cancel_traded_pairs_open_orders_if_any.assert_called_once()\n                    _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once()\n                    _get_rebalance_details_mock.assert_called_once()\n                    _trigger_rebalance_mock.assert_not_called()\n\n\nasync def test_cancel_traded_pairs_open_orders_if_any(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    orders = [\n        mock.Mock(symbol=\"BTC/USDT\"),\n        mock.Mock(symbol=\"BTC/USDT\"),\n        mock.Mock(symbol=\"ETH/USDT\"),\n        mock.Mock(symbol=\"DOGE/USDT\"),\n    ]\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.orders_manager, \"get_open_orders\", mock.Mock(return_value=orders)\n    ) as get_open_orders_mock, \\\n        mock.patch.object(mode, \"cancel_order\", mock.AsyncMock(return_value=(True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])))) \\\n            as cancel_order_mock:\n        assert await producer.cancel_traded_pairs_open_orders_if_any() == trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"123\")])\n        get_open_orders_mock.assert_called_once()\n        assert cancel_order_mock.call_count == 2\n        assert cancel_order_mock.mock_calls[0].args[0] is orders[0]\n        assert cancel_order_mock.mock_calls[1].args[0] is orders[1]\n\n\nasync def test_trigger_rebalance(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(\n            producer, \"submit_trading_evaluation\", mock.AsyncMock()\n    ) as _wait_for_symbol_prices_and_profitability_init_mock:\n        details = {\"hi\": \"ho\"}\n        await producer._trigger_rebalance(details, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n        _wait_for_symbol_prices_and_profitability_init_mock.assert_called_once_with(\n            cryptocurrency=None,\n            symbol=None,\n            time_frame=None,\n            final_note=None,\n            state=trading_enums.EvaluatorStates.NEUTRAL,\n            data=details,\n            dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        )\n\n\nasync def test_get_rebalance_details(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"BTC/USDT\", \"SOL/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    mode.rebalance_trigger_min_ratio = decimal.Decimal(\"0.1\")\n    portfolio_value_holder = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder\n    with mock.patch.object(producer, \"_resolve_swaps\", mock.Mock()) as _resolve_swaps_mock:\n        def _get_holdings_ratio(coin, **kwargs):\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0\")\n            return decimal.Decimal(\"0.3\")\n        with mock.patch.object(\n            portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            with mock.patch.object(\n                mode, \"get_removed_coins_from_config\", mock.Mock(return_value=[])\n            ) as get_removed_coins_from_config_mock:\n                should_rebalance, details = producer._get_rebalance_details()\n                assert should_rebalance is False\n                assert details == {\n                    index_trading.RebalanceDetails.SELL_SOME.value: {},\n                    index_trading.RebalanceDetails.BUY_MORE.value: {},\n                    index_trading.RebalanceDetails.REMOVE.value: {},\n                    index_trading.RebalanceDetails.ADD.value: {},\n                    index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n                }\n                assert get_holdings_ratio_mock.call_count == len(mode.indexed_coins) + 1  # +1 for USDT\n                get_removed_coins_from_config_mock.assert_called_once()\n                _resolve_swaps_mock.assert_called_once_with(details)\n                _resolve_swaps_mock.reset_mock()\n                get_holdings_ratio_mock.reset_mock()\n            with mock.patch.object(\n                    mode, \"get_removed_coins_from_config\", mock.Mock(return_value=[\"SOL\", \"ADA\"])\n            ) as get_removed_coins_from_config_mock:\n                should_rebalance, details = producer._get_rebalance_details()\n                assert should_rebalance is True\n                assert details == {\n                    index_trading.RebalanceDetails.SELL_SOME.value: {},\n                    index_trading.RebalanceDetails.BUY_MORE.value: {},\n                    index_trading.RebalanceDetails.REMOVE.value: {\n                        \"SOL\": decimal.Decimal(\"0.3\"),\n                        # \"ADA\": decimal.Decimal(\"0.3\")  # ADA is not in traded pairs, it's not removed\n                    },\n                    index_trading.RebalanceDetails.ADD.value: {},\n                    index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n                }\n                assert get_holdings_ratio_mock.call_count == \\\n                       len(mode.indexed_coins) + len(details[index_trading.RebalanceDetails.REMOVE.value]) + 1  # +1 for USDT\n                get_removed_coins_from_config_mock.assert_called_once()\n                _resolve_swaps_mock.assert_called_once_with(details)\n                _resolve_swaps_mock.reset_mock()\n                get_holdings_ratio_mock.reset_mock()\n        def _get_holdings_ratio(coin, **kwargs):\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0\")\n            return decimal.Decimal(\"0.2\")\n        with mock.patch.object(\n                portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            with mock.patch.object(\n                    mode, \"get_removed_coins_from_config\", mock.Mock(return_value=[])\n            ) as get_removed_coins_from_config_mock:\n                should_rebalance, details = producer._get_rebalance_details()\n                assert should_rebalance is True\n                assert details == {\n                    index_trading.RebalanceDetails.SELL_SOME.value: {},\n                    index_trading.RebalanceDetails.BUY_MORE.value: {\n                        'BTC': decimal.Decimal('0.3333333333333333617834929233'),\n                        'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                        'SOL': decimal.Decimal('0.3333333333333333617834929233')\n                    },\n                    index_trading.RebalanceDetails.REMOVE.value: {},\n                    index_trading.RebalanceDetails.ADD.value: {},\n                    index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n                }\n                assert get_holdings_ratio_mock.call_count == len(mode.indexed_coins) + 1  # +1 for USDT\n                get_removed_coins_from_config_mock.assert_called_once()\n                _resolve_swaps_mock.assert_called_once_with(details)\n                _resolve_swaps_mock.reset_mock()\n                get_holdings_ratio_mock.reset_mock()\n            with mock.patch.object(\n                    mode, \"get_removed_coins_from_config\", mock.Mock(return_value=[\"SOL\", \"ADA\"])\n            ) as get_removed_coins_from_config_mock:\n                should_rebalance, details = producer._get_rebalance_details()\n                assert should_rebalance is True\n                assert details == {\n                    index_trading.RebalanceDetails.SELL_SOME.value: {},\n                    index_trading.RebalanceDetails.BUY_MORE.value: {\n                        'BTC': decimal.Decimal('0.3333333333333333617834929233'),\n                        'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                        'SOL': decimal.Decimal('0.3333333333333333617834929233')\n                    },\n                    index_trading.RebalanceDetails.REMOVE.value: {\n                        \"SOL\": decimal.Decimal(\"0.2\"),\n                        # \"ADA\": decimal.Decimal(\"0.2\")  # not in traded pairs\n                    },\n                    index_trading.RebalanceDetails.ADD.value: {},\n                    index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n                }\n                assert get_holdings_ratio_mock.call_count == \\\n                       len(mode.indexed_coins) + len(details[index_trading.RebalanceDetails.REMOVE.value]) + 1  # +1 for USDT\n                get_removed_coins_from_config_mock.assert_called_once()\n                _resolve_swaps_mock.assert_called_once_with(details)\n                _resolve_swaps_mock.reset_mock()\n                get_holdings_ratio_mock.reset_mock()\n\n        # rebalance cap larger than ratio\n        def _get_holdings_ratio(coin, **kwargs):\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0\")\n            return decimal.Decimal(\"0.3\")\n        mode.rebalance_trigger_min_ratio = decimal.Decimal(\"0.5\")\n        with mock.patch.object(\n                portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is False\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {},\n                index_trading.RebalanceDetails.BUY_MORE.value: {},\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {},\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n            }\n            assert get_holdings_ratio_mock.call_count == len(mode.indexed_coins) + 1  # +1 for USDT\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_called_once_with(details)\n            _resolve_swaps_mock.reset_mock()\n        def _get_holdings_ratio(coin, **kwargs):\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0\")\n            return decimal.Decimal(\"0.00000001\")\n        with mock.patch.object(\n                portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is False\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {},\n                index_trading.RebalanceDetails.BUY_MORE.value: {},\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {},\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n            }\n            assert get_holdings_ratio_mock.call_count == len(mode.indexed_coins) + 1  # +1 for USDT\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_called_once_with(details)\n            _resolve_swaps_mock.reset_mock()\n        def _get_holdings_ratio(coin, **kwargs):\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0\")\n            return decimal.Decimal(\"0.9\")\n        with mock.patch.object(\n                portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is True\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {\n                    'BTC': decimal.Decimal('0.3333333333333333617834929233'),\n                    'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                    'SOL': decimal.Decimal('0.3333333333333333617834929233')\n                },\n                index_trading.RebalanceDetails.BUY_MORE.value: {},\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {},\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n            }\n            assert get_holdings_ratio_mock.call_count == len(details[index_trading.RebalanceDetails.SELL_SOME.value]) + 1  # +1 for USDT\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_called_once_with(details)\n            _resolve_swaps_mock.reset_mock()\n        def _get_holdings_ratio(coin, **kwargs):\n            return decimal.Decimal(\"0\")\n        with mock.patch.object(\n                portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is True\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {},\n                index_trading.RebalanceDetails.BUY_MORE.value: {},\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {\n                    'BTC': decimal.Decimal('0.3333333333333333617834929233'),\n                    'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                    'SOL': decimal.Decimal('0.3333333333333333617834929233')\n                },\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n            }\n            assert get_holdings_ratio_mock.call_count == len(details[index_trading.RebalanceDetails.ADD.value]) + 1  # +1 for USDT\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_called_once_with(details)\n            _resolve_swaps_mock.reset_mock()\n\n        # will only add ETH\n        def _get_holdings_ratio(coin, **kwargs):\n            if coin == \"ETH\":\n                return decimal.Decimal(\"0\")\n            return decimal.Decimal(\"0.33\")\n        with mock.patch.object(\n            portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is True\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {},\n                index_trading.RebalanceDetails.BUY_MORE.value: {},\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {\n                    'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                },\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n            }\n            assert get_holdings_ratio_mock.call_count == 3 + 1  # called for each coin + 1 for USDT\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_called_once_with(details)\n            _resolve_swaps_mock.reset_mock()\n        \nasync def test_get_rebalance_details_with_usdt_without_coin_distribution_update(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"BTC/USDT\", \"SOL/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    mode.rebalance_trigger_min_ratio = decimal.Decimal(\"0.1\")\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n    portfolio_value_holder = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder\n    with mock.patch.object(producer, \"_resolve_swaps\", mock.Mock()) as _resolve_swaps_mock, \\\n        mock.patch.object(mode, \"ensure_updated_coins_distribution\", mock.Mock()) as ensure_updated_coins_distribution_mock:\n        def _get_holdings_ratio(coin, **kwargs):\n            # USDT is 1/3 of the portfolio\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0.33\")\n            # other coins are 2/3 of the portfolio\n            return decimal.Decimal(\"0.33\") * decimal.Decimal(\"2\") / decimal.Decimal(\"3\")\n\n        # with added USDT to the portfolio\n        with mock.patch.object(\n            portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is True\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {},\n                index_trading.RebalanceDetails.BUY_MORE.value: {\n                    'BTC': decimal.Decimal('0.3333333333333333617834929233'),\n                    'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                    'SOL': decimal.Decimal('0.3333333333333333617834929233')\n                },\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {},\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: True,\n            }\n            assert get_holdings_ratio_mock.call_count == len(mode.indexed_coins) + 1  # called to check non-indexed assets ratio\n            ensure_updated_coins_distribution_mock.assert_not_called()\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_not_called()\n            _resolve_swaps_mock.reset_mock()\n        \nasync def test_get_rebalance_details_with_usdt_and_coin_distribution_update(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"BTC/USDT\", \"SOL/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    mode.rebalance_trigger_min_ratio = decimal.Decimal(\"0.1\")\n    portfolio_value_holder = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    with mock.patch.object(producer, \"_resolve_swaps\", mock.Mock()) as _resolve_swaps_mock, \\\n        mock.patch.object(mode, \"ensure_updated_coins_distribution\", mock.Mock()) as ensure_updated_coins_distribution_mock:\n        def _get_holdings_ratio(coin, **kwargs):\n            # USDT is 1/3 of the portfolio\n            if coin == \"USDT\":\n                return decimal.Decimal(\"0.33\")\n            # other coins are 2/3 of the portfolio\n            return decimal.Decimal(\"0.33\") * decimal.Decimal(\"2\") / decimal.Decimal(\"3\")\n\n        # with added USDT to the portfolio\n        with mock.patch.object(\n            portfolio_value_holder, \"get_holdings_ratio\", mock.Mock(side_effect=_get_holdings_ratio)\n        ) as get_holdings_ratio_mock:\n            should_rebalance, details = producer._get_rebalance_details()\n            assert should_rebalance is True\n            assert details == {\n                index_trading.RebalanceDetails.SELL_SOME.value: {},\n                index_trading.RebalanceDetails.BUY_MORE.value: {\n                    'BTC': decimal.Decimal('0.3333333333333333617834929233'),\n                    'ETH': decimal.Decimal('0.3333333333333333617834929233'),\n                    'SOL': decimal.Decimal('0.3333333333333333617834929233')\n                },\n                index_trading.RebalanceDetails.REMOVE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {},\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: True,\n            }\n            # 2 x called to check non-indexed assets ratio (once for current and one for latest distribution)\n            assert get_holdings_ratio_mock.call_count == 2 * (len(mode.indexed_coins) + 1)  \n            ensure_updated_coins_distribution_mock.assert_called_once()\n            get_holdings_ratio_mock.reset_mock()\n            _resolve_swaps_mock.assert_not_called()\n            _resolve_swaps_mock.reset_mock()\n\n\nasync def test_should_rebalance_due_to_non_indexed_quote_assets_ratio(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    assert mode.quote_asset_rebalance_ratio_threshold == decimal.Decimal(\"0.1\")\n    rebalance_details = {\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.23\"), rebalance_details) is True\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.1\"), rebalance_details) is True\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.09\"), rebalance_details) is False\n    # lower threshold\n    mode.quote_asset_rebalance_ratio_threshold = decimal.Decimal(\"0.05\")\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.09\"), rebalance_details) is True\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.04\"), rebalance_details) is False\n\n    # test added coins\n    rebalance_details[index_trading.RebalanceDetails.ADD.value] = {\n        \"BTC\": decimal.Decimal(\"0.1\")\n    }\n    rebalance_details[index_trading.RebalanceDetails.BUY_MORE.value] = {\n        \"ETH\": decimal.Decimal(\"0.1\")\n    }\n    # can't swap quote for BTC & ETH\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.1\"), rebalance_details) is True\n    # can swap quote for BTC & ETH: don't rebalance\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.2\"), rebalance_details) is False\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.21\"), rebalance_details) is False\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.18\"), rebalance_details) is False\n    # beyond QUOTE_ASSET_TO_INDEXED_SWAP_RATIO_THRESHOLD threshold\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.17\"), rebalance_details) is True\n\n    # with removed coins: can't \"just swap quote for added coins\", perform regular quote ratio check\n    rebalance_details[index_trading.RebalanceDetails.REMOVE.value] = {\n        \"BTC\": decimal.Decimal(\"0.1\")\n    }\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.2\"), rebalance_details) is True  # is False when no coins are to remove\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.03\"), rebalance_details) is False  # bellow threshold: still false\n\n    # with sell some coins and removed coins: can't \"just swap quote for added coins\", perform regular quote ratio check\n    rebalance_details[index_trading.RebalanceDetails.SELL_SOME.value] = {\n        \"BTC\": decimal.Decimal(\"0.1\")\n    }\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.2\"), rebalance_details) is True  # is False when no coins are to remove\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.03\"), rebalance_details) is False  # bellow threshold: still false\n    # with only sell some coin\n    rebalance_details[index_trading.RebalanceDetails.REMOVE.value] = {}\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.2\"), rebalance_details) is True  # is False when no coins are to remove\n    assert producer._should_rebalance_due_to_non_indexed_quote_assets_ratio(decimal.Decimal(\"0.03\"), rebalance_details) is False  # bellow threshold: still false\n\n\nasync def test_get_removed_coins_from_config_sell_removed_coins_asap(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n    mode.sell_unindexed_traded_coins = False\n    assert mode.get_removed_coins_from_config([]) == []\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"AA\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BB\"\n            }\n        ]\n    }\n    assert mode.get_removed_coins_from_config([]) == []\n    mode.previous_trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"AA\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BB\"\n            }\n        ]\n    }\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"AA\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"CC\"\n            }\n        ]\n    }\n    assert mode.get_removed_coins_from_config([]) == [\"BB\"]\n    # with sell_unindexed_traded_coins=True\n    mode.sell_unindexed_traded_coins = True\n    mode.indexed_coins = [\"BTC\"]\n    mode.previous_trading_config = None\n    assert mode.get_removed_coins_from_config([\"BTC\", \"ETH\"]) == [\"ETH\"]\n    mode.previous_trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"AA\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BB\"\n            }\n        ]\n    }\n    assert sorted(mode.get_removed_coins_from_config([\"BTC\", \"ETH\"])) == sorted([\"ETH\", \"BB\"])\n\n\nasync def test_get_removed_coins_from_config_sell_removed_on_ratio_rebalance(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    mode.sell_unindexed_traded_coins = False\n    assert mode.get_removed_coins_from_config([]) == []\n    # without historical config\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"SOL\"\n            }\n        ]\n    }\n    assert mode.get_removed_coins_from_config([]) == []\n    # with sell_unindexed_traded_coins=True\n    mode.sell_unindexed_traded_coins = True\n    mode.indexed_coins = [\"BTC\"]\n    assert mode.get_removed_coins_from_config([\"BTC\", \"ETH\"]) == [\"ETH\"]\n\n    # with historical config\n    historical_config_1 = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ADA\"\n            }\n        ]\n    }\n    historical_config_2 = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\"\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"DOT\"\n            }\n        ]\n    }\n    commons_configuration.add_historical_tentacle_config(mode.trading_config, 1, historical_config_1)\n    commons_configuration.add_historical_tentacle_config(mode.trading_config, 2, historical_config_2)\n    mode.historical_master_config = mode.trading_config\n    with mock.patch.object(mode.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=0)):\n        assert mode.get_removed_coins_from_config([\"BTC\", \"ETH\", \"SOL\"]) == [\"ETH\", \"SOL\"]\n    with mock.patch.object(mode.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=2)):\n        assert sorted(mode.get_removed_coins_from_config([\"BTC\", \"ETH\", \"SOL\"])) == sorted(\n            [\"ETH\", \"SOL\", \"ADA\", \"DOT\"]\n        )\n        assert sorted(mode.get_removed_coins_from_config([\"BTC\", \"ETH\"])) == sorted(['ADA', 'DOT', 'ETH'])\n\n    # with sell_unindexed_traded_coins=False\n    mode.sell_unindexed_traded_coins = False\n    with mock.patch.object(mode.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=0)):\n        assert mode.get_removed_coins_from_config([\"BTC\", \"ETH\", \"SOL\"]) == []\n    with mock.patch.object(mode.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=2)):\n        assert sorted(mode.get_removed_coins_from_config([\"BTC\", \"ETH\", \"SOL\"])) == sorted(\n            [\"ADA\", \"DOT\"]\n        )\n        assert sorted(mode.get_removed_coins_from_config([\"BTC\", \"ETH\"])) == sorted(['ADA', 'DOT'])\n\n\nasync def test_create_new_orders(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(\n            consumer, \"_rebalance_portfolio\", mock.AsyncMock(return_value=\"plop\")\n    ) as _rebalance_portfolio_mock:\n        assert mode.is_processing_rebalance is False\n        with pytest.raises(KeyError):\n            # missing \"data\"\n            await consumer.create_new_orders(None, None, None)\n        assert await consumer.create_new_orders(None, None, None, data=\"hello\", dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == []\n        assert mode.is_processing_rebalance is False\n        _rebalance_portfolio_mock.assert_not_called()\n        assert await consumer.create_new_orders(\n            None, None, trading_enums.EvaluatorStates.NEUTRAL.value, data=\"hello\", dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        ) == \"plop\"\n        _rebalance_portfolio_mock.assert_called_once_with(\"hello\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n        assert mode.is_processing_rebalance is False\n\n\nasync def test_rebalance_portfolio(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    sell_order = mock.Mock(order_id=\"456\")\n    with mock.patch.object(\n            consumer, \"_ensure_enough_funds_to_buy_after_selling\", mock.AsyncMock()\n    ) as _ensure_enough_funds_to_buy_after_selling_mock, mock.patch.object(\n        consumer, \"_sell_indexed_coins_for_reference_market\", mock.AsyncMock(return_value=[sell_order])\n    ) as _sell_indexed_coins_for_reference_market_mock, mock.patch.object(\n        consumer, \"_split_reference_market_into_indexed_coins\", mock.AsyncMock(return_value=[\"buy\"])\n    ) as _split_reference_market_into_indexed_coins_mock:\n        with mock.patch.object(\n            consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=False)\n        ) as _can_simply_buy_coins_without_selling_mock:\n            assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == [sell_order, \"buy\"]\n            _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n            _sell_indexed_coins_for_reference_market_mock.assert_called_once_with(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n            _split_reference_market_into_indexed_coins_mock.assert_called_once_with(\"details\", False, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456\")]))\n            _can_simply_buy_coins_without_selling_mock.assert_called_once_with(\"details\")\n            _ensure_enough_funds_to_buy_after_selling_mock.reset_mock()\n            _sell_indexed_coins_for_reference_market_mock.reset_mock()\n            _split_reference_market_into_indexed_coins_mock.reset_mock()\n        with mock.patch.object(\n            consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=True)\n        ) as _can_simply_buy_coins_without_selling_mock:\n            assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == [\"buy\"]\n            _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n            _sell_indexed_coins_for_reference_market_mock.assert_not_called()\n            _split_reference_market_into_indexed_coins_mock.assert_called_once_with(\"details\", True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n            _can_simply_buy_coins_without_selling_mock.assert_called_once_with(\"details\")\n\n    with mock.patch.object(\n            consumer, \"_update_producer_last_activity\", mock.Mock()\n    ) as _update_producer_last_activity_mock:\n        with mock.patch.object(\n                consumer, \"_ensure_enough_funds_to_buy_after_selling\", mock.AsyncMock()\n        ) as _ensure_enough_funds_to_buy_after_selling_mock, mock.patch.object(\n            consumer, \"_sell_indexed_coins_for_reference_market\", mock.AsyncMock(\n                side_effect=trading_errors.MissingMinimalExchangeTradeVolume\n            )\n        ) as _sell_indexed_coins_for_reference_market_mock, mock.patch.object(\n            consumer, \"_split_reference_market_into_indexed_coins\", mock.AsyncMock(return_value=[\"buy\"])\n        ) as _split_reference_market_into_indexed_coins_mock, mock.patch.object(\n            consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=False)\n        ) as _can_simply_buy_coins_without_selling_mock:\n            assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == []\n            _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n            _sell_indexed_coins_for_reference_market_mock.assert_called_once_with(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n            _split_reference_market_into_indexed_coins_mock.assert_not_called()\n            _can_simply_buy_coins_without_selling_mock.assert_called_once_with(\"details\")\n            _update_producer_last_activity_mock.assert_called_once_with(\n                index_trading.IndexActivity.REBALANCING_SKIPPED,\n                index_trading.RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n            )\n            _update_producer_last_activity_mock.reset_mock()\n\n        with mock.patch.object(\n            consumer, \"_ensure_enough_funds_to_buy_after_selling\", mock.AsyncMock(\n                side_effect=trading_errors.MissingMinimalExchangeTradeVolume\n            )\n        ) as _ensure_enough_funds_to_buy_after_selling_mock, \\\n            mock.patch.object(\n                consumer, \"_sell_indexed_coins_for_reference_market\", mock.AsyncMock(return_value=[sell_order])\n        ) as _sell_indexed_coins_for_reference_market_mock, mock.patch.object(\n            consumer, \"_split_reference_market_into_indexed_coins\", mock.AsyncMock(return_value=[\"buy\"])\n        ) as _split_reference_market_into_indexed_coins_mock, mock.patch.object(\n            consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=False)\n        ) as _can_simply_buy_coins_without_selling_mock:\n            assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == []\n            _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n            _sell_indexed_coins_for_reference_market_mock.assert_not_called()\n            _split_reference_market_into_indexed_coins_mock.assert_not_called()\n            _can_simply_buy_coins_without_selling_mock.assert_not_called()\n            _update_producer_last_activity_mock.assert_called_once_with(\n                index_trading.IndexActivity.REBALANCING_SKIPPED,\n                index_trading.RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n            )\n            _update_producer_last_activity_mock.reset_mock()\n\n        with mock.patch.object(\n            consumer, \"_ensure_enough_funds_to_buy_after_selling\", mock.AsyncMock()\n        ) as _ensure_enough_funds_to_buy_after_selling_mock, \\\n        mock.patch.object(\n            consumer, \"_sell_indexed_coins_for_reference_market\", mock.AsyncMock(return_value=[sell_order])\n        ) as _sell_indexed_coins_for_reference_market_mock, mock.patch.object(\n            consumer, \"_split_reference_market_into_indexed_coins\", mock.AsyncMock(\n                side_effect=trading_errors.MissingMinimalExchangeTradeVolume\n            )\n        ) as _split_reference_market_into_indexed_coins_mock:\n            with mock.patch.object(\n                consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=False)\n            ) as _can_simply_buy_coins_without_selling_mock:\n                assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == [sell_order]\n                _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n                _sell_indexed_coins_for_reference_market_mock.assert_called_once_with(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n                _split_reference_market_into_indexed_coins_mock.assert_called_once_with(\"details\", False, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456\")]))\n                _update_producer_last_activity_mock.assert_called_once_with(\n                    index_trading.IndexActivity.REBALANCING_SKIPPED,\n                    index_trading.RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n                )\n                _ensure_enough_funds_to_buy_after_selling_mock.reset_mock()\n                _sell_indexed_coins_for_reference_market_mock.reset_mock()\n                _split_reference_market_into_indexed_coins_mock.reset_mock()\n                _update_producer_last_activity_mock.reset_mock()\n            with mock.patch.object(\n                consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=True)\n            ) as _can_simply_buy_coins_without_selling_mock:\n                assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == []\n                _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n                _sell_indexed_coins_for_reference_market_mock.assert_not_called()\n                _split_reference_market_into_indexed_coins_mock.assert_called_once_with(\"details\", True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n                _update_producer_last_activity_mock.assert_called_once_with(\n                    index_trading.IndexActivity.REBALANCING_SKIPPED,\n                    index_trading.RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n                )\n                _ensure_enough_funds_to_buy_after_selling_mock.reset_mock()\n                _sell_indexed_coins_for_reference_market_mock.reset_mock()\n                _split_reference_market_into_indexed_coins_mock.reset_mock()\n                _update_producer_last_activity_mock.reset_mock()\n\n        with mock.patch.object(\n            consumer, \"_ensure_enough_funds_to_buy_after_selling\", mock.AsyncMock()\n        ) as _ensure_enough_funds_to_buy_after_selling_mock, \\\n        mock.patch.object(\n            consumer, \"_sell_indexed_coins_for_reference_market\", mock.AsyncMock(return_value=[sell_order])\n        ) as _sell_indexed_coins_for_reference_market_mock, mock.patch.object(\n            consumer, \"_split_reference_market_into_indexed_coins\", mock.AsyncMock(\n                side_effect=index_trading.RebalanceAborted\n            )\n        ) as _split_reference_market_into_indexed_coins_mock:\n            with mock.patch.object(\n                consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=False)\n            ) as _can_simply_buy_coins_without_selling_mock:\n                assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == [sell_order]\n                _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n                _sell_indexed_coins_for_reference_market_mock.assert_called_once_with(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n                _split_reference_market_into_indexed_coins_mock.assert_called_once_with(\"details\", False, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456\")]))\n                _update_producer_last_activity_mock.assert_called_once_with(\n                    index_trading.IndexActivity.REBALANCING_SKIPPED,\n                    index_trading.RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n                )\n                _ensure_enough_funds_to_buy_after_selling_mock.reset_mock()\n                _sell_indexed_coins_for_reference_market_mock.reset_mock()\n                _split_reference_market_into_indexed_coins_mock.reset_mock()\n                _update_producer_last_activity_mock.reset_mock()\n            with mock.patch.object(\n                consumer, \"_can_simply_buy_coins_without_selling\", mock.Mock(return_value=True)\n            ) as _can_simply_buy_coins_without_selling_mock:\n                assert await consumer._rebalance_portfolio(\"details\", trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == []\n                _ensure_enough_funds_to_buy_after_selling_mock.assert_called_once()\n                _sell_indexed_coins_for_reference_market_mock.assert_not_called()\n                _split_reference_market_into_indexed_coins_mock.assert_called_once_with(\"details\", True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n                _update_producer_last_activity_mock.assert_called_once_with(\n                    index_trading.IndexActivity.REBALANCING_SKIPPED,\n                    index_trading.RebalanceSkipDetails.NOT_ENOUGH_AVAILABLE_FOUNDS.value\n                )\n                _ensure_enough_funds_to_buy_after_selling_mock.reset_mock()\n                _sell_indexed_coins_for_reference_market_mock.reset_mock()\n                _split_reference_market_into_indexed_coins_mock.reset_mock()\n                _update_producer_last_activity_mock.reset_mock()\n\n\nasync def test_ensure_enough_funds_to_buy_after_selling(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    with mock.patch.object(\n            trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n            \"get_traded_assets_holdings_value\", mock.Mock(return_value=decimal.Decimal(\"2000\"))\n    ) as get_traded_assets_holdings_value_mock, mock.patch.object(\n        consumer, \"_get_symbols_and_amounts\", mock.AsyncMock()\n    ) as _get_symbols_and_amounts_mock:\n        await consumer._ensure_enough_funds_to_buy_after_selling()\n        get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n        _get_symbols_and_amounts_mock.assert_called_once_with([\"BTC\"], decimal.Decimal(\"2000\"))\n\n\nasync def test_can_simply_buy_coins_without_selling(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    details = \"details\"\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_traded_assets_holdings_value\", mock.Mock(return_value=decimal.Decimal(\"2000\"))\n    ) as get_traded_assets_holdings_value_mock:\n\n        # no coins to simply buy\n        with mock.patch.object(\n            consumer, \"_get_simple_buy_coins\", return_value=[]\n        ) as _get_simple_buy_coins_mock, mock.patch.object(\n            trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n            \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"160\")))\n        ) as get_currency_portfolio_mock:\n            assert consumer._can_simply_buy_coins_without_selling(details) is False\n            _get_simple_buy_coins_mock.assert_called_once_with(details)\n            get_traded_assets_holdings_value_mock.assert_not_called()\n            get_currency_portfolio_mock.assert_not_called()\n\n        # there are coins to simply buy\n        with mock.patch.object(\n            mode, \"get_target_ratio\", return_value=decimal.Decimal(\"0.25\")\n        ) as get_target_ratio_mock, mock.patch.object(\n            consumer, \"_get_simple_buy_coins\", return_value=[\"BTC\"]\n        ) as _get_simple_buy_coins_mock:\n\n            # not enough free funds in portfolio to buy for 25% of 2000\n            with mock.patch.object(\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"160\")))\n            ) as get_currency_portfolio_mock:\n                assert consumer._can_simply_buy_coins_without_selling(details) is False\n                _get_simple_buy_coins_mock.assert_called_once_with(details)\n                get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                get_target_ratio_mock.assert_called_once_with(\"BTC\")\n                _get_simple_buy_coins_mock.reset_mock()\n                get_traded_assets_holdings_value_mock.reset_mock()\n                get_target_ratio_mock.reset_mock()\n\n            # enough free funds in portfolio to buy for 25% of 2000 (using tolerance)\n            with mock.patch.object(\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"450\")))\n            ) as get_currency_portfolio_mock:\n                assert consumer._can_simply_buy_coins_without_selling(details) is True\n                _get_simple_buy_coins_mock.assert_called_once_with(details)\n                get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                get_target_ratio_mock.assert_called_once_with(\"BTC\")\n                _get_simple_buy_coins_mock.reset_mock()\n                get_traded_assets_holdings_value_mock.reset_mock()\n                get_target_ratio_mock.reset_mock()\n\n            # more than enough free funds in portfolio to buy for 25% of 2000\n            with mock.patch.object(\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"600.811\")))\n            ) as get_currency_portfolio_mock:\n                assert consumer._can_simply_buy_coins_without_selling(details) is True\n                _get_simple_buy_coins_mock.assert_called_once_with(details)\n                get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                get_target_ratio_mock.assert_called_once_with(\"BTC\")\n                _get_simple_buy_coins_mock.reset_mock()\n                get_traded_assets_holdings_value_mock.reset_mock()\n                get_target_ratio_mock.reset_mock()\n\n            # now having multiple coins to buy\n            with  mock.patch.object(\n                consumer, \"_get_simple_buy_coins\", return_value=[\"BTC\", \"ETH\"]\n            ) as _get_simple_buy_coins_mock:\n                # enough funds for 1 but not 2 coins at 25%\n                with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\",\n                    mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"600.811\")))\n                ) as get_currency_portfolio_mock:\n                    assert consumer._can_simply_buy_coins_without_selling(details) is False\n                    _get_simple_buy_coins_mock.assert_called_once_with(details)\n                    get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                    get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                    assert get_target_ratio_mock.call_count == 2\n                    assert get_target_ratio_mock.mock_calls[0].args[0] == \"BTC\"\n                    assert get_target_ratio_mock.mock_calls[1].args[0] == \"ETH\"\n                    _get_simple_buy_coins_mock.reset_mock()\n                    get_traded_assets_holdings_value_mock.reset_mock()\n                    get_target_ratio_mock.reset_mock()\n\n                # enough funds for 2 coins at 25%\n                with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\",\n                    mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"1000.811\")))\n                ) as get_currency_portfolio_mock:\n                    assert consumer._can_simply_buy_coins_without_selling(details) is True\n                    _get_simple_buy_coins_mock.assert_called_once_with(details)\n                    get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                    get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                    assert get_target_ratio_mock.call_count == 2\n                    assert get_target_ratio_mock.mock_calls[0].args[0] == \"BTC\"\n                    assert get_target_ratio_mock.mock_calls[1].args[0] == \"ETH\"\n                    _get_simple_buy_coins_mock.reset_mock()\n                    get_traded_assets_holdings_value_mock.reset_mock()\n                    get_target_ratio_mock.reset_mock()\n\n\nasync def test_get_simple_buy_coins(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.indexed_coins = [\"BTC\", \"ETH\", \"SOL\"]\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == []\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"BTC\": decimal.Decimal(\"0.2\"), \"ETH\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"ETH\"]\n    # keep index coins order\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {\"SOL\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"ETH\": decimal.Decimal(\"0.2\"), \"BTC\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"ETH\", \"SOL\"]\n    # TRX not in indexed coins: added at the end\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {\"SOL\": decimal.Decimal(\"0.1\"), \"TRX\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"ETH\": decimal.Decimal(\"0.2\"), \"BTC\": decimal.Decimal(\"0.5\")},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"ETH\", \"SOL\", \"TRX\"]\n\n    # don't return anything when other values are set\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {\"BTC\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"ETH\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == []\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {\"BTC\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.ADD.value: {\"ETH\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == []\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"ETH\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {\"BTC\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == []\n    # whatever is in other values, return [] when forced rebalance\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"ETH\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {\"BTC\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: True,\n    }) == []\n    # should return [BTC, ETH] but doesn't because of forced rebalance\n    assert consumer._get_simple_buy_coins({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {\"BTC\": decimal.Decimal(\"0.2\"), \"ETH\": decimal.Decimal(\"0.2\")},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: True,\n    }) == []\n\n\nasync def test_sell_indexed_coins_for_reference_market(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    orders = [\n        mock.Mock(\n            symbol=\"BTC/USDT\",\n            side=trading_enums.TradeOrderSide.SELL\n        ),\n        mock.Mock(\n            symbol=\"ETH/USDT\",\n            side=trading_enums.TradeOrderSide.SELL\n        )\n    ]\n    with mock.patch.object(\n            octobot_trading.modes, \"convert_assets_to_target_asset\", mock.AsyncMock(return_value=orders)\n    ) as convert_assets_to_target_asset_mock, mock.patch.object(\n        trading_personal_data, \"wait_for_order_fill\", mock.AsyncMock()\n    ) as wait_for_order_fill_mock, mock.patch.object(\n        consumer, \"_get_coins_to_sell\", mock.Mock(return_value=[1, 2, 3])\n    ) as _get_coins_to_sell_mock:\n        details = {\n            index_trading.RebalanceDetails.REMOVE.value: {}\n        }\n        assert await consumer._sell_indexed_coins_for_reference_market(details, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == orders\n        convert_assets_to_target_asset_mock.assert_called_once_with(\n            mode, [1, 2, 3],\n            consumer.exchange_manager.exchange_personal_data.portfolio_manager.reference_market, {},\n            dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        )\n        assert wait_for_order_fill_mock.call_count == 2\n        _get_coins_to_sell_mock.assert_called_once_with(details)\n        convert_assets_to_target_asset_mock.reset_mock()\n        wait_for_order_fill_mock.reset_mock()\n        _get_coins_to_sell_mock.reset_mock()\n\n        # with valid remove coins\n        details = {\n            index_trading.RebalanceDetails.REMOVE.value: {\"BTC\": 0.01},\n            index_trading.RebalanceDetails.BUY_MORE.value: {},\n            index_trading.RebalanceDetails.ADD.value: {},\n            index_trading.RebalanceDetails.SWAP.value: {},\n            index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n        }\n        assert await consumer._sell_indexed_coins_for_reference_market(details, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == orders + orders\n        assert convert_assets_to_target_asset_mock.call_count == 2\n        assert wait_for_order_fill_mock.call_count == 4\n        _get_coins_to_sell_mock.assert_called_once_with(details)\n        convert_assets_to_target_asset_mock.reset_mock()\n        wait_for_order_fill_mock.reset_mock()\n        _get_coins_to_sell_mock.reset_mock()\n\n        with mock.patch.object(\n                octobot_trading.modes, \"convert_assets_to_target_asset\", mock.AsyncMock(return_value=[])\n        ) as convert_assets_to_target_asset_mock_2:\n            # with remove coins that can't be sold\n            details = {\n                index_trading.RebalanceDetails.REMOVE.value: {\"BTC\": 0.01},\n                index_trading.RebalanceDetails.BUY_MORE.value: {},\n                index_trading.RebalanceDetails.ADD.value: {},\n                index_trading.RebalanceDetails.SWAP.value: {},\n                index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n            }\n            with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n                assert await consumer._sell_indexed_coins_for_reference_market(details, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])) == orders + orders\n            convert_assets_to_target_asset_mock_2.assert_called_once_with(\n                mode, [\"BTC\"],\n                consumer.exchange_manager.exchange_personal_data.portfolio_manager.reference_market, {},\n                dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            )\n            wait_for_order_fill_mock.assert_not_called()\n            _get_coins_to_sell_mock.assert_not_called()\n\n\nasync def test_get_coins_to_sell(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.indexed_coins = [\"BTC\", \"ETH\", \"DOGE\", \"SHIB\"]\n    assert consumer._get_coins_to_sell({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"ETH\", \"DOGE\", \"SHIB\"]\n    assert consumer._get_coins_to_sell({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {\n            \"BTC\": \"ETH\"\n        },\n    }) == [\"BTC\"]\n    assert consumer._get_coins_to_sell({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {\n            \"XRP\": trading_constants.ONE_HUNDRED\n        },\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {\n            \"BTC\": \"ETH\",\n            \"SOL\": \"ADA\",\n        },\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"SOL\"]\n    assert consumer._get_coins_to_sell({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"ETH\", \"DOGE\", \"SHIB\"]\n    assert consumer._get_coins_to_sell({\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {\n            \"XRP\": trading_constants.ONE_HUNDRED\n        },\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {},\n        index_trading.RebalanceDetails.FORCED_REBALANCE.value: False,\n    }) == [\"BTC\", \"ETH\", \"DOGE\", \"SHIB\"]\n\n\nasync def test_resolve_swaps(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    mode.rebalance_trigger_min_ratio = decimal.Decimal(\"0.05\")  # %5\n    rebalance_details = {\n        index_trading.RebalanceDetails.SELL_SOME.value: {},\n        index_trading.RebalanceDetails.BUY_MORE.value: {},\n        index_trading.RebalanceDetails.REMOVE.value: {},\n        index_trading.RebalanceDetails.ADD.value: {},\n        index_trading.RebalanceDetails.SWAP.value: {},\n    }\n    # regular full rebalance\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n    # regular full rebalance with removed coins to sell\n    rebalance_details[index_trading.RebalanceDetails.REMOVE.value] = {\"SOL\": decimal.Decimal(\"0.3\")}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n    # rebalances with a coin swap only from ADD coin\n    rebalance_details[index_trading.RebalanceDetails.ADD.value] = {\"ADA\": decimal.Decimal(\"0.3\")}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {\"SOL\": \"ADA\"}\n\n    # rebalances with a coin swap only from BUY_MORE coin\n    rebalance_details[index_trading.RebalanceDetails.ADD.value] = {}\n    rebalance_details[index_trading.RebalanceDetails.BUY_MORE.value] = {\"ADA\": decimal.Decimal(\"0.3\")}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {\"SOL\": \"ADA\"}\n    rebalance_details[index_trading.RebalanceDetails.BUY_MORE.value] = {}\n\n    # rebalances with an incompatible coin swap (ratio too different)\n    rebalance_details[index_trading.RebalanceDetails.BUY_MORE.value] = {\"ADA\": decimal.Decimal(\"0.1\")}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n    rebalance_details[index_trading.RebalanceDetails.BUY_MORE.value] = {}\n\n    # rebalances with an incompatible coin swap (ratio too different)\n    rebalance_details[index_trading.RebalanceDetails.ADD.value] = {\"ADA\": decimal.Decimal(\"0.5\")}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n    # rebalances with 2 removed coins: sell everything\n    rebalance_details[index_trading.RebalanceDetails.REMOVE.value] = {\n        \"SOL\": decimal.Decimal(\"0.3\"),\n        \"XRP\": decimal.Decimal(\"0.3\"),\n    }\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n    # rebalances with 2 coin swaps: sell everything\n    rebalance_details[index_trading.RebalanceDetails.ADD.value] = {\n        \"ADA\": decimal.Decimal(\"0.3\"),\n        \"ADA2\": decimal.Decimal(\"0.3\"),\n    }\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n    # rebalance with regular buy / sell more\n    rebalance_details[index_trading.RebalanceDetails.BUY_MORE.value] = {\"LTC\": decimal.Decimal(1)}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n    # rebalance with regular buy / sell more\n    rebalance_details[index_trading.RebalanceDetails.SELL_SOME.value] = {\"BTC\": decimal.Decimal(1)}\n    producer._resolve_swaps(rebalance_details)\n    assert rebalance_details[index_trading.RebalanceDetails.SWAP.value] == {}\n\n\nasync def test_split_reference_market_into_indexed_coins(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    # no indexed coin\n    mode.indexed_coins = []\n    details = {index_trading.RebalanceDetails.SWAP.value: {}}\n    is_simple_buy_without_selling = False\n    dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n    with mock.patch.object(\n        consumer, \"_get_symbols_and_amounts\", mock.AsyncMock(\n            side_effect=lambda coins, _: {f\"{coin}/USDT\": decimal.Decimal(i + 1) for i, coin in enumerate(coins)}\n        )\n    ) as _get_symbols_and_amounts_mock:\n        with mock.patch.object(\n            consumer, \"_get_simple_buy_coins\", mock.Mock()\n        ) as _get_simple_buy_coins_mock:\n            with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"2\")))\n            ) as get_currency_portfolio_mock, mock.patch.object(\n                consumer, \"_buy_coin\", mock.AsyncMock(return_value=[\"order\"])\n            ) as _buy_coin_mock:\n                with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n                    await consumer._split_reference_market_into_indexed_coins(details, is_simple_buy_without_selling, dependencies)\n                get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                _buy_coin_mock.assert_not_called()\n                _get_symbols_and_amounts_mock.assert_called_once()\n                _get_symbols_and_amounts_mock.reset_mock()\n                _get_simple_buy_coins_mock.assert_not_called()\n\n            # coins to swap\n            mode.indexed_coins = []\n            details = {index_trading.RebalanceDetails.SWAP.value: {\"BTC\": \"ETH\", \"ADA\": \"SOL\"}}\n            with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"2\")))\n            ) as get_currency_portfolio_mock, mock.patch.object(\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n                \"get_traded_assets_holdings_value\", mock.Mock(return_value=decimal.Decimal(\"2000\"))\n            ) as get_traded_assets_holdings_value_mock, mock.patch.object(\n                consumer, \"_buy_coin\", mock.AsyncMock(return_value=[\"order\"])\n            ) as _buy_coin_mock:\n                assert await consumer._split_reference_market_into_indexed_coins(\n                    details, is_simple_buy_without_selling, dependencies\n                ) == [\"order\", \"order\"]\n                _get_symbols_and_amounts_mock.assert_called_once()\n                _get_symbols_and_amounts_mock.reset_mock()\n                get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                get_currency_portfolio_mock.assert_not_called()\n                _get_simple_buy_coins_mock.assert_not_called()\n                assert _buy_coin_mock.call_count == 2\n                assert _buy_coin_mock.mock_calls[0].args == (\"ETH/USDT\", decimal.Decimal(\"1\"), dependencies)\n                assert _buy_coin_mock.mock_calls[1].args == (\"SOL/USDT\", decimal.Decimal(\"2\"), dependencies)\n\n            # no bought coin\n            details = {index_trading.RebalanceDetails.SWAP.value: {}}\n            mode.indexed_coins = [\"ETH\", \"BTC\"]\n            with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"2\")))\n            ) as get_currency_portfolio_mock, mock.patch.object(\n                consumer, \"_buy_coin\", mock.AsyncMock(return_value=[])\n            ) as _buy_coin_mock:\n                with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n                    await consumer._split_reference_market_into_indexed_coins(details, is_simple_buy_without_selling, dependencies)\n                _get_symbols_and_amounts_mock.assert_called_once()\n                _get_symbols_and_amounts_mock.reset_mock()\n                get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                _get_simple_buy_coins_mock.assert_not_called()\n                assert _buy_coin_mock.call_count == 2\n\n            # bought coins\n            mode.indexed_coins = [\"ETH\", \"BTC\"]\n            with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"2\")))\n            ) as get_currency_portfolio_mock, mock.patch.object(\n                consumer, \"_buy_coin\", mock.AsyncMock(return_value=[\"order\"])\n            ) as _buy_coin_mock:\n                assert await consumer._split_reference_market_into_indexed_coins(\n                    details, is_simple_buy_without_selling, dependencies\n                ) == [\"order\", \"order\"]\n                _get_symbols_and_amounts_mock.assert_called_once()\n                _get_symbols_and_amounts_mock.reset_mock()\n                get_currency_portfolio_mock.assert_called_once_with(\"USDT\")\n                _get_simple_buy_coins_mock.assert_not_called()\n                assert _buy_coin_mock.call_count == 2\n                assert _buy_coin_mock.mock_calls[0].args[0] == \"ETH/USDT\"\n                assert _buy_coin_mock.mock_calls[0].args[2] == dependencies\n                assert _buy_coin_mock.mock_calls[1].args[0] == \"BTC/USDT\"\n                assert _buy_coin_mock.mock_calls[1].args[2] == dependencies\n\n        with mock.patch.object(\n            consumer, \"_get_simple_buy_coins\", mock.Mock(return_value=[\"ETH\"])\n        ) as _get_simple_buy_coins_mock:\n            # simple buy without selling => buying only ETH\n            is_simple_buy_without_selling = True\n            mode.indexed_coins = [\"ETH\", \"BTC\"]\n            with mock.patch.object(\n                    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio,\n                    \"get_currency_portfolio\", mock.Mock(return_value=mock.Mock(available=decimal.Decimal(\"2\")))\n            ) as get_currency_portfolio_mock, mock.patch.object(\n                consumer, \"_buy_coin\", mock.AsyncMock(return_value=[\"order\"])\n            ) as _buy_coin_mock, mock.patch.object(\n                trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n                \"get_traded_assets_holdings_value\", mock.Mock(return_value=decimal.Decimal(\"2000\"))\n            ) as get_traded_assets_holdings_value_mock:\n                assert await consumer._split_reference_market_into_indexed_coins(\n                    details, is_simple_buy_without_selling, dependencies\n                ) == [\"order\"]\n                _get_symbols_and_amounts_mock.assert_called_once()\n                _get_symbols_and_amounts_mock.reset_mock()\n                get_currency_portfolio_mock.assert_not_called()\n                get_traded_assets_holdings_value_mock.assert_called_once_with(\"USDT\", None)\n                _get_simple_buy_coins_mock.assert_called_once_with(details)\n                assert _buy_coin_mock.call_count == 1\n                assert _buy_coin_mock.mock_calls[0].args[0] == \"ETH/USDT\"\n                assert _buy_coin_mock.mock_calls[0].args[2] == dependencies\n\nasync def test_get_symbols_and_amounts(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"BTC/USDT\"]\n    ]\n    mode.ensure_updated_coins_distribution()\n    assert await consumer._get_symbols_and_amounts([\"BTC\"], decimal.Decimal(3000)) == {\n        \"BTC/USDT\": decimal.Decimal(3)\n    }\n    with mock.patch.object(\n            trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=decimal.Decimal(1000))\n    ) as get_up_to_date_price_mock:\n        assert await consumer._get_symbols_and_amounts([\"BTC\", \"ETH\"], decimal.Decimal(3000)) == {\n            \"BTC/USDT\": decimal.Decimal(3)\n        }\n        assert get_up_to_date_price_mock.call_count == 2\n        get_up_to_date_price_mock.reset_mock()\n        trader.exchange_manager.exchange_config.traded_symbols = [\n            commons_symbols.parse_symbol(symbol)\n            for symbol in [\"BTC/USDT\", \"ETH/USDT\"]\n        ]\n        mode.ensure_updated_coins_distribution()\n        assert await consumer._get_symbols_and_amounts([\"BTC\", \"ETH\"], decimal.Decimal(3000)) == {\n            \"BTC/USDT\": decimal.Decimal(\"1.5\"),\n            \"ETH/USDT\": decimal.Decimal(\"1.5\")\n        }\n        assert get_up_to_date_price_mock.call_count == 2\n\n    # not enough funds\n    with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n        await consumer._get_symbols_and_amounts([\"BTC\"], decimal.Decimal(0.0003))\n    with mock.patch.object(\n            trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=decimal.Decimal(0.000000001))\n    ) as get_up_to_date_price_mock:\n        with pytest.raises(trading_errors.MissingMinimalExchangeTradeVolume):\n            await consumer._get_symbols_and_amounts([\"BTC\", \"ETH\"], decimal.Decimal(0.01))\n        assert get_up_to_date_price_mock.call_count == 1\n\n    # with ref market in coins config\n    mode.trading_config = {\n        \"index_content\": [\n            {\n                \"name\": \"BTC\",\n                \"value\": 70\n            },\n            {\n                \"name\": \"USDT\",\n                \"value\": 30\n            }\n        ],\n        \"refresh_interval\": 1,\n        \"required_strategies\": [],\n        \"rebalance_trigger_min_percent\": 5\n    }\n    mode.ensure_updated_coins_distribution()\n    with mock.patch.object(\n            trading_personal_data, \"get_up_to_date_price\", mock.AsyncMock(return_value=decimal.Decimal(1000))\n    ) as get_up_to_date_price_mock:\n        # USDT is not counted in orders to create (nothing to buy as USDT is the reference market everything is sold to)\n        assert await consumer._get_symbols_and_amounts([\"BTC\", \"USDT\"], decimal.Decimal(3000)) == {\n            \"BTC/USDT\": decimal.Decimal(\"2.1\")\n        }\n        assert get_up_to_date_price_mock.call_count == 1\n\n\nasync def test_buy_coin(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n    dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n    with mock.patch.object(mode, \"create_order\", mock.AsyncMock(side_effect=lambda x, **kwargs: x)) as create_order_mock:\n        # coin already held\n        portfolio[\"BTC\"].available = decimal.Decimal(20)\n        assert await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies) == []\n        create_order_mock.assert_not_called()\n\n        # coin already partially held\n        portfolio[\"BTC\"].available = decimal.Decimal(0.5)\n        orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies)\n        assert len(orders) == 1\n        create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n        assert isinstance(orders[0], trading_personal_data.BuyMarketOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].origin_price == decimal.Decimal(1000)\n        assert orders[0].origin_quantity == decimal.Decimal(\"1.5\")\n        assert orders[0].total_cost == decimal.Decimal(\"1500\")\n        create_order_mock.reset_mock()\n\n        # coin not already held\n        portfolio[\"BTC\"].available = decimal.Decimal(0)\n        orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies)\n        assert len(orders) == 1\n        create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n        assert isinstance(orders[0], trading_personal_data.BuyMarketOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].origin_price == decimal.Decimal(1000)\n        assert orders[0].origin_quantity == decimal.Decimal(2)\n        assert orders[0].total_cost == decimal.Decimal(\"2000\")\n        create_order_mock.reset_mock()\n\n        # given ideal_amount is lower\n        orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(\"0.025\"), dependencies)\n        assert len(orders) == 1\n        create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n        assert isinstance(orders[0], trading_personal_data.BuyMarketOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].origin_price == decimal.Decimal(1000)\n        assert orders[0].origin_quantity == decimal.Decimal(\"0.025\")  # use 100 instead of all 2000 USDT in pf\n        assert orders[0].total_cost == decimal.Decimal(\"25\")\n        create_order_mock.reset_mock()\n\n        # adapt for fees\n        fee_usdt_cost = decimal.Decimal(10)\n        with mock.patch.object(\n                consumer.exchange_manager.exchange, \"get_trade_fee\", mock.Mock(return_value={\n                    trading_enums.FeePropertyColumns.COST.value: str(fee_usdt_cost),\n                    trading_enums.FeePropertyColumns.CURRENCY.value: \"USDT\",\n                })\n        ) as get_trade_fee_mock:\n            orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(\"0.5\"), dependencies)\n            assert get_trade_fee_mock.call_count == 2\n            assert len(orders) == 1\n            create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n            assert isinstance(orders[0], trading_personal_data.BuyMarketOrder)\n            assert orders[0].symbol == \"BTC/USDT\"\n            assert orders[0].origin_price == decimal.Decimal(1000)\n            # no adaptation needed as not all funds are used (1/4 ratio)\n            assert orders[0].origin_quantity == decimal.Decimal(\"0.5\")\n            assert orders[0].total_cost == decimal.Decimal(\"500\")\n            create_order_mock.reset_mock()\n            get_trade_fee_mock.reset_mock()\n\n            orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies)\n            assert get_trade_fee_mock.call_count == 2\n            assert len(orders) == 1\n            create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n            assert isinstance(orders[0], trading_personal_data.BuyMarketOrder)\n            assert orders[0].symbol == \"BTC/USDT\"\n            assert orders[0].origin_price == decimal.Decimal(1000)\n            btc_fees = fee_usdt_cost / orders[0].origin_price\n            # 2 - fees denominated in BTC\n            assert orders[0].origin_quantity == decimal.Decimal(\"2\") - btc_fees * trading_constants.FEES_SAFETY_MARGIN\n            assert orders[0].total_cost == decimal.Decimal('1987.5000')\n            create_order_mock.reset_mock()\n\n\nasync def test_buy_coin_using_limit_order(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    portfolio = trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n    dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n    with mock.patch.object(\n            mode,\n            \"create_order\", mock.AsyncMock(side_effect=lambda x, **kwargs: x)\n    ) as create_order_mock, mock.patch.object(\n            mode.exchange_manager.exchange,\n            \"is_market_open_for_order_type\", mock.Mock(return_value=False)\n    ) as is_market_open_for_order_type_mock:\n        # coin already held\n        portfolio[\"BTC\"].available = decimal.Decimal(20)\n        assert await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies) == []\n        create_order_mock.assert_not_called()\n        is_market_open_for_order_type_mock.assert_not_called()\n\n        # coin already partially held: buy more using limit order\n        portfolio[\"BTC\"].available = decimal.Decimal(0.5)\n        orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies)\n        assert len(orders) == 1\n        is_market_open_for_order_type_mock.assert_called_once_with(\"BTC/USDT\", trading_enums.TraderOrderType.BUY_MARKET)\n        create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n        assert isinstance(orders[0], trading_personal_data.BuyLimitOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].origin_price == decimal.Decimal(1005)  # a bit above market price to instant fill\n        assert orders[0].origin_quantity == decimal.Decimal('1.49253731')  # reduced a bit to compensate price increase\n        assert decimal.Decimal(\"1499.99999\") < orders[0].total_cost < decimal.Decimal(\"1500\")\n        create_order_mock.reset_mock()\n        is_market_open_for_order_type_mock.reset_mock()\n\n        # coin not already held\n        portfolio[\"BTC\"].available = decimal.Decimal(0)\n        orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies)\n        assert len(orders) == 1\n        is_market_open_for_order_type_mock.assert_called_once_with(\"BTC/USDT\", trading_enums.TraderOrderType.BUY_MARKET)\n        create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n        assert isinstance(orders[0], trading_personal_data.BuyLimitOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].origin_price == decimal.Decimal('1005.000')\n        assert orders[0].origin_quantity == decimal.Decimal('1.99004975')\n        assert decimal.Decimal(\"1999.99999\") < orders[0].total_cost < decimal.Decimal(\"2000\")\n        create_order_mock.reset_mock()\n        is_market_open_for_order_type_mock.reset_mock()\n\n        # given ideal_amount is lower\n        orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(\"0.025\"), dependencies)\n        assert len(orders) == 1\n        is_market_open_for_order_type_mock.assert_called_once_with(\"BTC/USDT\", trading_enums.TraderOrderType.BUY_MARKET)\n        create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n        assert isinstance(orders[0], trading_personal_data.BuyLimitOrder)\n        assert orders[0].symbol == \"BTC/USDT\"\n        assert orders[0].origin_price == decimal.Decimal(1005)\n        assert orders[0].origin_quantity == decimal.Decimal('0.02487562')  # use 100 instead of all 2000 USDT in pf\n        assert decimal.Decimal('24.999') < orders[0].total_cost < decimal.Decimal(\"25\")\n        create_order_mock.reset_mock()\n        is_market_open_for_order_type_mock.reset_mock()\n\n        # adapt for fees\n        fee_usdt_cost = decimal.Decimal(10)\n        with mock.patch.object(\n                consumer.exchange_manager.exchange, \"get_trade_fee\", mock.Mock(return_value={\n                    trading_enums.FeePropertyColumns.COST.value: str(fee_usdt_cost),\n                    trading_enums.FeePropertyColumns.CURRENCY.value: \"USDT\",\n                })\n        ) as get_trade_fee_mock:\n            orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(\"0.5\"), dependencies)\n            assert get_trade_fee_mock.call_count == 2\n            assert len(orders) == 1\n            is_market_open_for_order_type_mock.assert_called_once_with(\"BTC/USDT\", trading_enums.TraderOrderType.BUY_MARKET)\n            create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n            assert isinstance(orders[0], trading_personal_data.BuyLimitOrder)\n            assert orders[0].symbol == \"BTC/USDT\"\n            assert orders[0].origin_price == decimal.Decimal(1005)\n            # no adaptation needed as not all funds are used (1/4 ratio)\n            assert orders[0].origin_quantity == decimal.Decimal('0.49751243')\n            assert decimal.Decimal('499.999') < orders[0].total_cost < decimal.Decimal(\"500\")\n            create_order_mock.reset_mock()\n            get_trade_fee_mock.reset_mock()\n            is_market_open_for_order_type_mock.reset_mock()\n\n            orders = await consumer._buy_coin(\"BTC/USDT\", decimal.Decimal(2), dependencies)\n            assert get_trade_fee_mock.call_count == 2\n            assert len(orders) == 1\n            is_market_open_for_order_type_mock.assert_called_once_with(\"BTC/USDT\", trading_enums.TraderOrderType.BUY_MARKET)\n            create_order_mock.assert_called_once_with(orders[0], dependencies=dependencies)\n            assert isinstance(orders[0], trading_personal_data.BuyLimitOrder)\n            assert orders[0].symbol == \"BTC/USDT\"\n            assert orders[0].origin_price == decimal.Decimal(1005)\n            # 2 - fees denominated in BTC\n            symbol_market = trader.exchange_manager.exchange.get_market_status(orders[0].symbol, with_fixer=False)\n            assert orders[0].origin_quantity == trading_personal_data.decimal_adapt_quantity(\n                symbol_market,\n                (\n                    decimal.Decimal(\"2000\") - fee_usdt_cost * trading_constants.FEES_SAFETY_MARGIN\n                ) / orders[0].origin_price\n            )\n            assert decimal.Decimal('1985') < orders[0].total_cost < decimal.Decimal('1990')\n            create_order_mock.reset_mock()\n            is_market_open_for_order_type_mock.reset_mock()\n\n\nasync def _get_tools(symbol=\"BTC/USDT\"):\n    config = test_config.load_test_config()\n    config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n    exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n    exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n    # use backtesting not to spam exchanges apis\n    exchange_manager.is_simulated = True\n    exchange_manager.is_backtesting = True\n    exchange_manager.use_cached_markets = False\n    backtesting = await backtesting_api.initialize_backtesting(\n        config,\n        exchange_ids=[exchange_manager.id],\n        matrix_id=None,\n        data_files=[os.path.join(test_config.TEST_CONFIG_FOLDER,\n                                 \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n    exchange_manager.exchange = exchanges.ExchangeSimulator(\n        exchange_manager.config, exchange_manager, backtesting\n    )\n    await exchange_manager.exchange.initialize()\n    exchange_manager.exchange_config.set_config_traded_pairs()\n    for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n        await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                         exchange_manager=exchange_manager)\n\n    trader = exchanges.TraderSimulator(config, exchange_manager)\n    await trader.initialize()\n    exchange_manager.exchange_personal_data.portfolio_manager.reference_market = \"USDT\"\n\n    mode = Mode.IndexTradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    # trading mode is not initialized: to be initialized with the required config in tests\n\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n\n    # set BTC/USDT price at 1000 USDT\n    trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n    return mode, trader\n\n\nasync def _init_mode(tools, config):\n    mode, trader = tools\n    await mode.initialize(trading_config=config)\n    return mode, mode.producers[0], mode.get_trading_mode_consumers()[0], trader\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n\n\nasync def test_automatically_update_historical_config_on_set_intervals(tools):\n    update = {}\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, update))\n    \n    # Test with SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE policy\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n    with mock.patch.object(mode, \"supports_historical_config\", mock.Mock(return_value=True)) as supports_historical_config_mock:\n        assert mode.automatically_update_historical_config_on_set_intervals() is True\n        supports_historical_config_mock.assert_called_once()\n        supports_historical_config_mock.reset_mock()\n    \n    with mock.patch.object(mode, \"supports_historical_config\", mock.Mock(return_value=False)) as supports_historical_config_mock:\n        assert mode.automatically_update_historical_config_on_set_intervals() is False\n        supports_historical_config_mock.assert_called_once()\n        supports_historical_config_mock.reset_mock()\n    \n    # Test with SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE policy\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    with mock.patch.object(mode, \"supports_historical_config\", mock.Mock(return_value=True)) as supports_historical_config_mock:\n        assert mode.automatically_update_historical_config_on_set_intervals() is False\n        supports_historical_config_mock.assert_called_once()\n        supports_historical_config_mock.reset_mock()\n    \n    with mock.patch.object(mode, \"supports_historical_config\", mock.Mock(return_value=False)) as supports_historical_config_mock:\n        assert mode.automatically_update_historical_config_on_set_intervals() is False\n        supports_historical_config_mock.assert_called_once()\n        supports_historical_config_mock.reset_mock()\n\n\nasync def test_ensure_updated_coins_distribution(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"ETH/USDT\", \"SOL/USDT\", \"BTC/USDT\"]\n    ]\n    distribution = [\n        {\n            index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n        },\n        {\n            index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n        },\n        {\n            index_trading.index_distribution.DISTRIBUTION_NAME: \"SOL\",\n            index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n        },\n    ]\n    with mock.patch.object(mode, \"_get_supported_distribution\", mock.Mock(return_value=distribution)) as _get_supported_distribution_mock:\n        mode.ensure_updated_coins_distribution()\n        _get_supported_distribution_mock.assert_called_once()\n        _get_supported_distribution_mock.reset_mock()\n        assert mode.ratio_per_asset == {\n            \"BTC\": {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            \"ETH\": {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            \"SOL\": {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"SOL\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            }\n        }\n        assert mode.total_ratio_per_asset == 100\n        assert mode.indexed_coins == [\"BTC\", \"ETH\", \"SOL\"]\n    \n    # include ref market in distribution\n    distribution = [\n        {\n            index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n            index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n        },\n        {\n            index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n            index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n        },\n        {\n            index_trading.index_distribution.DISTRIBUTION_NAME: \"USDT\",\n            index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n        },\n    ]\n    with mock.patch.object(mode, \"_get_supported_distribution\", mock.Mock(return_value=distribution)) as _get_supported_distribution_mock:\n        mode.ensure_updated_coins_distribution()\n        _get_supported_distribution_mock.assert_called_once()\n        _get_supported_distribution_mock.reset_mock()\n        assert mode.ratio_per_asset == {\n            \"BTC\": {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            \"ETH\": {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            \"USDT\": {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"USDT\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            }\n        }\n        assert mode.total_ratio_per_asset == 100\n        assert mode.indexed_coins == [\"BTC\", \"ETH\", \"USDT\"]\n\n\nasync def test_get_supported_distribution(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"BTC/USDT\", \"ETH/USDT\", \"SOL/USDT\", \"ADA/USDT\"]\n    ]\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT:  [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 25\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 25\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"SOL\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 25\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ADA\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 25\n            },\n        ]\n    }\n    with mock.patch.object(mode, \"get_ideal_distribution\", mock.Mock(wraps=mode.get_ideal_distribution)) as get_ideal_distribution_mock:\n        # no ideal distribution: return uniform distribution over traded assets\n        assert mode._get_supported_distribution(False, False) == mode.trading_config[\n            index_trading.IndexTradingModeProducer.INDEX_CONTENT\n        ]\n        get_ideal_distribution_mock.assert_called_once()\n\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT:  [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"USDT\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            },\n        ]\n    }\n    with mock.patch.object(mode, \"get_ideal_distribution\", mock.Mock(wraps=mode.get_ideal_distribution)) as get_ideal_distribution_mock:\n        assert mode._get_supported_distribution(False, False) == mode.trading_config[\n            index_trading.IndexTradingModeProducer.INDEX_CONTENT\n        ]\n        get_ideal_distribution_mock.assert_called_once()\n\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT:  [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"USDT\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"PLOP\", # not traded\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            },\n        ]\n    }\n    with mock.patch.object(mode, \"get_ideal_distribution\", mock.Mock(wraps=mode.get_ideal_distribution)) as get_ideal_distribution_mock:\n        assert mode._get_supported_distribution(False, False) == [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"USDT\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            },\n            # {\n            #     index_trading.index_distribution.DISTRIBUTION_NAME: \"PLOP\", # not traded\n            #     index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            # },\n        ]\n        get_ideal_distribution_mock.assert_called_once()\n\n    mode.trading_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT:  [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"USDT\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 20\n            },\n        ]\n    }\n\n    # synchronization policy is not SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_AS_SOON_AS_POSSIBLE\n    with mock.patch.object(mode, \"get_ideal_distribution\", mock.Mock(wraps=mode.get_ideal_distribution)) as get_ideal_distribution_mock:\n        with mock.patch.object(mode, \"_get_currently_applied_historical_config_according_to_holdings\", mock.Mock()) as _get_currently_applied_historical_config_according_to_holdings_mock, \\\n            mock.patch.object(mode, \"get_historical_configs\", mock.Mock()) as get_historical_configs_mock:\n            assert mode._get_supported_distribution(True, False) == mode.trading_config[\n                index_trading.IndexTradingModeProducer.INDEX_CONTENT\n            ]\n            get_ideal_distribution_mock.assert_called_once()\n            _get_currently_applied_historical_config_according_to_holdings_mock.assert_not_called()\n            get_historical_configs_mock.assert_not_called()\n            _get_currently_applied_historical_config_according_to_holdings_mock.reset_mock()\n            get_historical_configs_mock.reset_mock()\n            get_ideal_distribution_mock.reset_mock()\n            assert mode._get_supported_distribution(False, True) == mode.trading_config[\n                index_trading.IndexTradingModeProducer.INDEX_CONTENT\n            ]\n            get_ideal_distribution_mock.assert_called_once()\n            _get_currently_applied_historical_config_according_to_holdings_mock.assert_not_called()\n            get_historical_configs_mock.assert_not_called()\n    \n    # synchronization policy is SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    mode.synchronization_policy = index_trading.SynchronizationPolicy.SELL_REMOVED_INDEX_COINS_ON_RATIO_REBALANCE\n    holding_adapted_config = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n        ]\n    }\n    with mock.patch.object(mode, \"get_ideal_distribution\", mock.Mock(wraps=mode.get_ideal_distribution)) as get_ideal_distribution_mock:\n        with mock.patch.object(mode, \"_get_currently_applied_historical_config_according_to_holdings\", mock.Mock(return_value=holding_adapted_config)) as _get_currently_applied_historical_config_according_to_holdings_mock, \\\n            mock.patch.object(mode, \"get_historical_configs\", mock.Mock()) as get_historical_configs_mock:\n            assert mode._get_supported_distribution(True, False) == holding_adapted_config[\n                index_trading.IndexTradingModeProducer.INDEX_CONTENT\n            ]\n            assert get_ideal_distribution_mock.call_count == 2\n            _get_currently_applied_historical_config_according_to_holdings_mock.assert_called_once_with(\n                mode.trading_config, {'ADA', 'BTC', 'SOL', 'USDT', 'ETH'}\n            )\n            get_historical_configs_mock.assert_not_called()\n            get_ideal_distribution_mock.reset_mock()\n        \n        # with historical configs\n        latest_config = {\n            index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n                {\n                    index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                    index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n                },\n            ]\n        }\n        historical_configs = [\n            latest_config,\n            holding_adapted_config,\n\n        ]\n        with mock.patch.object(mode, \"_get_currently_applied_historical_config_according_to_holdings\", mock.Mock()) as _get_currently_applied_historical_config_according_to_holdings_mock, \\\n            mock.patch.object(mode, \"get_historical_configs\", mock.Mock(return_value=historical_configs)) as get_historical_configs_mock:\n            assert mode._get_supported_distribution(False, True) == latest_config[\n                index_trading.IndexTradingModeProducer.INDEX_CONTENT\n            ]\n            assert get_ideal_distribution_mock.call_count == 3\n            _get_currently_applied_historical_config_according_to_holdings_mock.assert_not_called()\n            get_historical_configs_mock.assert_called_once_with(\n                0, mode.exchange_manager.exchange.get_exchange_current_time()\n            )\n            get_ideal_distribution_mock.reset_mock()\n\n        # without historical configs\n        with mock.patch.object(mode, \"_get_currently_applied_historical_config_according_to_holdings\", mock.Mock()) as _get_currently_applied_historical_config_according_to_holdings_mock, \\\n            mock.patch.object(mode, \"get_historical_configs\", mock.Mock(return_value=[])) as get_historical_configs_mock:\n            # use current config\n            assert mode._get_supported_distribution(False, True) == mode.trading_config[\n                index_trading.IndexTradingModeProducer.INDEX_CONTENT\n            ]\n            assert get_ideal_distribution_mock.call_count == 2\n            _get_currently_applied_historical_config_according_to_holdings_mock.assert_not_called()\n            get_historical_configs_mock.assert_called_once_with(\n                0, mode.exchange_manager.exchange.get_exchange_current_time()\n            )\n            get_ideal_distribution_mock.reset_mock()\n\n\nasync def test_get_currently_applied_historical_config_according_to_holdings(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"BTC/USDT\", \"ETH/USDT\", \"SOL/USDT\", \"ADA/USDT\"]\n    ]\n    traded_bases = set(\n        symbol.base\n        for symbol in trader.exchange_manager.exchange_config.traded_symbols\n    )\n    # 1. using latest config\n    with mock.patch.object(mode, \"_is_index_config_applied\", mock.Mock(return_value=True)) as _is_index_config_applied_mock:\n        assert mode._get_currently_applied_historical_config_according_to_holdings(\n            mode.trading_config, traded_bases\n        ) == mode.trading_config\n        _is_index_config_applied_mock.assert_called_once_with(mode.trading_config, traded_bases)\n\n    # 2. using historical configs\n    with mock.patch.object(mode, \"_is_index_config_applied\", mock.Mock(return_value=False)) as _is_index_config_applied_mock, mock.patch.object(mode.exchange_manager.exchange, \"get_exchange_current_time\", mock.Mock(return_value=2)) as get_exchange_current_time_mock:\n        # 2.1. no historical configs\n        assert mode._get_currently_applied_historical_config_according_to_holdings(\n            mode.trading_config, traded_bases\n        ) == mode.trading_config\n        _is_index_config_applied_mock.assert_called_once_with(mode.trading_config, traded_bases)\n        _is_index_config_applied_mock.reset_mock()\n        get_exchange_current_time_mock.assert_called_once()\n        get_exchange_current_time_mock.reset_mock()\n\n        # 2.2. with historical configs but as _is_index_config_applied always return False, fallback to current config\n        hist_config_1 = {\n            index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n                {\n                    index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                    index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n                },\n                {\n                    index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                    index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n                },\n            ]\n        }\n        hist_config_2 = {\n            index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n                {\n                    index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                    index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n                },\n            ]\n        }\n        commons_configuration.add_historical_tentacle_config(mode.trading_config, 1, hist_config_1)\n        commons_configuration.add_historical_tentacle_config(mode.trading_config, 2, hist_config_2)\n        mode.historical_master_config = mode.trading_config\n        assert mode._get_currently_applied_historical_config_according_to_holdings(\n            mode.trading_config, traded_bases\n        ) == mode.trading_config\n        assert _is_index_config_applied_mock.call_count == 3\n        assert _is_index_config_applied_mock.mock_calls[0].args[0] == mode.trading_config\n        assert _is_index_config_applied_mock.mock_calls[1].args[0] == hist_config_2\n        assert _is_index_config_applied_mock.mock_calls[2].args[0] == hist_config_1\n        _is_index_config_applied_mock.reset_mock()\n        get_exchange_current_time_mock.assert_called_once()\n        get_exchange_current_time_mock.reset_mock()\n\n        __is_index_config_applied_calls = []\n        accepted_config_index = 1\n        def __is_index_config_applied(*args):\n            __is_index_config_applied_calls.append(1)\n            if len(__is_index_config_applied_calls) - 1 >= accepted_config_index:\n                return True\n            return False\n\n        # 2.3. with historical configs using historical config\n        with mock.patch.object(mode, \"_is_index_config_applied\", mock.Mock(side_effect=__is_index_config_applied)) as _is_index_config_applied_mock:\n            # 1. use most up to date config\n            assert mode._get_currently_applied_historical_config_according_to_holdings(\n                mode.trading_config, traded_bases\n            ) == hist_config_2\n            assert _is_index_config_applied_mock.call_count == 2\n            assert _is_index_config_applied_mock.mock_calls[0].args[0] == mode.trading_config\n            assert _is_index_config_applied_mock.mock_calls[1].args[0] == hist_config_2\n            _is_index_config_applied_mock.reset_mock()\n            get_exchange_current_time_mock.assert_called_once()\n            get_exchange_current_time_mock.reset_mock()\n\n        __is_index_config_applied_calls.clear()\n        accepted_config_index = 2\n        with mock.patch.object(mode, \"_is_index_config_applied\", mock.Mock(side_effect=__is_index_config_applied)) as _is_index_config_applied_mock:\n            # 2. use oldest config\n            assert mode._get_currently_applied_historical_config_according_to_holdings(\n                mode.trading_config, traded_bases\n            ) == hist_config_1\n            assert _is_index_config_applied_mock.call_count == 3\n            assert _is_index_config_applied_mock.mock_calls[0].args[0] == mode.trading_config\n            assert _is_index_config_applied_mock.mock_calls[1].args[0] == hist_config_2\n            assert _is_index_config_applied_mock.mock_calls[2].args[0] == hist_config_1\n            _is_index_config_applied_mock.reset_mock()\n\n\nasync def test_is_index_config_applied(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    trader.exchange_manager.exchange_config.traded_symbols = [\n        commons_symbols.parse_symbol(symbol)\n        for symbol in [\"BTC/USDT\", \"ETH/USDT\", \"SOL/USDT\", \"ADA/USDT\"]\n    ]\n    traded_bases = set(\n        symbol.base\n        for symbol in trader.exchange_manager.exchange_config.traded_symbols\n    )\n    \n    # Test 1: No ideal distribution - should return False\n    config_without_distribution = {}\n    assert mode._is_index_config_applied(config_without_distribution, traded_bases) is False\n    \n    # Test 2: Empty ideal distribution - should return False\n    config_with_empty_distribution = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: []\n    }\n    assert mode._is_index_config_applied(config_with_empty_distribution, traded_bases) is False\n    \n    # Test 3: Distribution with only non-traded assets - should return False\n    config_with_non_traded_assets = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"NON_TRADED_COIN\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 100\n            }\n        ]\n    }\n    assert mode._is_index_config_applied(config_with_non_traded_assets, traded_bases) is False\n    \n    # Test 4: Distribution with zero total ratio - should return False\n    config_with_zero_total = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 0\n            }\n        ]\n    }\n    assert mode._is_index_config_applied(config_with_zero_total, traded_bases) is False\n    \n    # Test 5: Valid distribution with holdings matching target ratios\n    config_with_valid_distribution = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 60\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 40\n            }\n        ]\n    }\n    \n    # Mock holdings ratios to match target ratios exactly\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.6\"),  # 60% target\n            \"ETH\": decimal.Decimal(\"0.4\"),  # 40% target\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_valid_distribution, traded_bases) is True\n        assert get_holdings_ratio_mock.call_count == 2\n        assert get_holdings_ratio_mock.mock_calls[0].args[0] == \"BTC\"\n        assert get_holdings_ratio_mock.mock_calls[1].args[0] == \"ETH\"\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 6: Valid distribution with holdings within tolerance range\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.62\"),  # 60% target + 2% (within 5% tolerance)\n            \"ETH\": decimal.Decimal(\"0.38\"),  # 40% target - 2% (within 5% tolerance)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_valid_distribution, traded_bases) is True\n        assert get_holdings_ratio_mock.call_count == 2\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 7: Holdings outside tolerance range - should return False\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.68\"),  # 60% target + 8% (outside 5% tolerance)\n            \"ETH\": decimal.Decimal(\"0.32\"),  # 40% target - 8% (outside 5% tolerance)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_valid_distribution, traded_bases) is False\n        assert get_holdings_ratio_mock.call_count == 1  # only BTC is considered\n        get_holdings_ratio_mock.assert_called_once_with(\"BTC\", traded_symbols_only=True)\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 8: Missing coin in portfolio - should return False\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.6\"),  # 60% target\n            \"ETH\": decimal.Decimal(\"0\"),     # Missing ETH\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_valid_distribution, traded_bases) is False\n        assert get_holdings_ratio_mock.call_count == 2\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 9: Too much of a coin in portfolio - should return False\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.6\"),  # 60% target: OK\n            \"ETH\": decimal.Decimal(\"0.3\"),  # 40% target - 10% (too little)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_valid_distribution, traded_bases) is False\n        assert get_holdings_ratio_mock.call_count == 2  # BTC and ETH considered\n        assert get_holdings_ratio_mock.mock_calls[0].args[0] == \"BTC\"\n        assert get_holdings_ratio_mock.mock_calls[1].args[0] == \"ETH\"\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 10a: Custom rebalance trigger ratio in config from REBALANCE_TRIGGER_MIN_PERCENT\n    config_with_custom_trigger = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            }\n        ],\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 10.0  # 10% tolerance\n    }\n    \n    # Holdings within 10% tolerance\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.57\"),  # 50% target + 7% (within 10% tolerance)\n            \"ETH\": decimal.Decimal(\"0.43\"),  # 50% target - 7% (within 10% tolerance)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_custom_trigger, traded_bases) is True\n        assert get_holdings_ratio_mock.call_count == 2\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Holdings outside 10% tolerance\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.65\"),  # 50% target + 15% (outside 10% tolerance)\n            \"ETH\": decimal.Decimal(\"0.35\"),  # 50% target - 15% (outside 10% tolerance)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_custom_trigger, traded_bases) is False\n        assert get_holdings_ratio_mock.call_count == 1  # only BTC is considered\n        get_holdings_ratio_mock.assert_called_once_with(\"BTC\", traded_symbols_only=True)\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 10b: Custom rebalance trigger ratio in config from REBALANCE_TRIGGER_MIN_PERCENT\n    config_with_custom_trigger = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            }\n        ],\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 10.0  # 10% tolerance\n            }\n        ],\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"profile-1\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 99.0  # 99% tolerance\n    }\n    \n    # Holdings within 10% tolerance (profile 1)\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.57\"),  # 50% target + 7% (within 10% tolerance)\n            \"ETH\": decimal.Decimal(\"0.43\"),  # 50% target - 7% (within 10% tolerance)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_custom_trigger, traded_bases) is True\n        assert get_holdings_ratio_mock.call_count == 2\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Holdings outside 10% tolerance (profile 1)\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.65\"),  # 50% target + 15% (outside 10% tolerance)\n            \"ETH\": decimal.Decimal(\"0.35\"),  # 50% target - 15% (outside 10% tolerance)\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_custom_trigger, traded_bases) is False\n        assert get_holdings_ratio_mock.call_count == 1  # only BTC is considered\n        get_holdings_ratio_mock.assert_called_once_with(\"BTC\", traded_symbols_only=True)\n        get_holdings_ratio_mock.reset_mock()\n    \n    # Test 11: Mixed traded and non-traded assets\n    config_with_mixed_assets = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"BTC\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 60\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"ETH\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 30\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"NON_TRADED_COIN\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 10\n            }\n        ]\n    }\n    \n    # Should only consider traded assets (BTC and ETH)\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(side_effect=lambda coin, **kwargs: {\n            \"BTC\": decimal.Decimal(\"0.6666666666666666666666666667\"),  # 60/90 = 66.67%\n            \"ETH\": decimal.Decimal(\"0.3333333333333333333333333333\"),  # 30/90 = 33.33%\n        }.get(coin, decimal.Decimal(\"0\")))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_mixed_assets, traded_bases) is False\n        get_holdings_ratio_mock.assert_not_called()\n    \n    # Test 12: All assets non-traded\n    config_all_non_traded = {\n        index_trading.IndexTradingModeProducer.INDEX_CONTENT: [\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"NON_TRADED_1\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            },\n            {\n                index_trading.index_distribution.DISTRIBUTION_NAME: \"NON_TRADED_2\",\n                index_trading.index_distribution.DISTRIBUTION_VALUE: 50\n            }\n        ]\n    }\n    assert mode._is_index_config_applied(config_all_non_traded, traded_bases) is False\n    \n    # Test 13: Zero holdings for all coins\n    with mock.patch.object(\n        trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder,\n        \"get_holdings_ratio\", mock.Mock(return_value=decimal.Decimal(\"0\"))\n    ) as get_holdings_ratio_mock:\n        assert mode._is_index_config_applied(config_with_valid_distribution, traded_bases) is False\n        assert get_holdings_ratio_mock.call_count == 1  # only BTC considered\n        get_holdings_ratio_mock.assert_called_once_with(\"BTC\", traded_symbols_only=True)\n        get_holdings_ratio_mock.reset_mock()\n\n\nasync def test_get_config_min_ratio(tools):\n    mode, producer, consumer, trader = await _init_mode(tools, _get_config(tools, {}))\n    # 1. With selected profile\n    config_with_profiles = {\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 7.5,\n            },\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-2\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 15.0,\n            },\n        ],\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"profile-2\",\n    }\n    # Should pick 15.0% from profile-2\n    assert mode._get_config_min_ratio(config_with_profiles) == decimal.Decimal(\"0.15\")\n\n    # 2. With direct config value only\n    config_with_direct = {\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 3.3\n    }\n    # Should pick 3.3% from direct config\n    assert mode._get_config_min_ratio(config_with_direct) == decimal.Decimal(\"0.033\")\n\n    # 3. With neither, should fall back to mode.rebalance_trigger_min_ratio\n    mode.rebalance_trigger_min_ratio = decimal.Decimal(\"0.123\")\n    config_empty = {}\n    assert mode._get_config_min_ratio(config_empty) == decimal.Decimal(\"0.123\")\n\n    # 4. With profiles but no selected profile matches, should fall back to direct config\n    config_profiles_no_match = {\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILES: [\n            {\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_NAME: \"profile-1\",\n                index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_PROFILE_MIN_PERCENT: 7.5,\n            }\n        ],\n        index_trading.IndexTradingModeProducer.SELECTED_REBALANCE_TRIGGER_PROFILE: \"profile-x\",\n        index_trading.IndexTradingModeProducer.REBALANCE_TRIGGER_MIN_PERCENT: 2.2\n    }\n    assert mode._get_config_min_ratio(config_profiles_no_match) == decimal.Decimal(\"0.022\")\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/__init__.py",
    "content": "from .market_making_trading import MarketMakingTradingMode\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/config/MarketMakingTradingMode.json",
    "content": "{\n  \"required_strategies\": [],\n  \"asks_count\": 3,\n  \"bids_count\": 3,\n  \"min_spread\": 2,\n  \"max_spread\": 10,\n  \"reference_exchange\": \"local\"\n}"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/market_making_trading.py",
    "content": "# pylint: disable=E701\n# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport asyncio\nimport collections\nimport dataclasses\nimport decimal\nimport typing\n\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.pretty_printer\nimport octobot_tentacles_manager.api\nimport octobot_trading.api as trading_api\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.exchanges as trading_exchanges\nimport octobot_tentacles_manager.configuration as tm_configuration\nimport tentacles.Trading.Mode.market_making_trading_mode.order_book_distribution as order_book_distribution\nimport tentacles.Trading.Mode.market_making_trading_mode.reference_price as reference_price_import\n\n\n@dataclasses.dataclass\nclass OrderData:\n    side: trading_enums.TradeOrderSide = None\n    quantity: decimal.Decimal = trading_constants.ZERO\n    price: decimal.Decimal = trading_constants.ZERO\n    symbol: str = 0\n\n\nclass OrderAction:\n    pass\n\n\n@dataclasses.dataclass\nclass CreateOrderAction(OrderAction):\n    order_data: OrderData\n\n    @classmethod\n    def from_book_order_data(cls, symbol, order: order_book_distribution.BookOrderData):\n        return cls(\n            OrderData(\n                side=order.side,\n                quantity=order.amount,\n                price=order.price,\n                symbol=symbol,\n            )\n        )\n\n\n@dataclasses.dataclass\nclass CancelOrderAction(OrderAction):\n    order: trading_personal_data.Order\n\n\n@dataclasses.dataclass\nclass OrdersUpdatePlan:\n    order_actions: list[OrderAction] = dataclasses.field(default_factory=list)\n    cancelled: bool = False\n    cancellable: bool = True\n    force_cancelled: bool = False\n    processed: asyncio.Event = dataclasses.field(default_factory=asyncio.Event)\n    trigger_source: str = \"\"\n\n    def __str__(self):\n        cancel_actions = [a for a in self.order_actions if isinstance(a, CancelOrderAction)]\n        create_actions = [a for a in self.order_actions if isinstance(a, CreateOrderAction)]\n        return (\n            f\"{self.__class__.__name__} of {len(self.order_actions)} {OrderAction.__name__} [{len(cancel_actions)} \"\n            f\"{CancelOrderAction.__name__} & {len(create_actions)} {CreateOrderAction.__name__}], \"\n            f\"cancelled: {self.cancelled} cancellable: {self.cancellable} \"\n            f\"[trigger_source: {self.trigger_source}]\"\n        )\n\n\nclass SkippedAction(Exception):\n    pass\n\n\nclass MarketMakingTradingMode(trading_modes.AbstractTradingMode):\n    REQUIRE_TRADES_HISTORY = False   # set True when this trading mode needs the trade history to operate\n    SUPPORTS_INITIAL_PORTFOLIO_OPTIMIZATION = False  # set True when self._optimize_initial_portfolio is implemented\n    SUPPORTS_HEALTH_CHECK = False   # set True when self.health_check is implemented\n\n    MIN_SPREAD = \"min_spread\"\n    MAX_SPREAD = \"max_spread\"\n    BIDS_COUNT = \"bids_count\"\n    ASKS_COUNT = \"asks_count\"\n    REFERENCE_EXCHANGE = \"reference_exchange\"\n    LOCAL_EXCHANGE_PRICE = \"local\"\n\n    MIN_SPREAD_DESC = \"Min spread %: Percentage of the current price to use as bid-ask spread.\"\n    MAX_SPREAD_DESC = \"Max spread %: Percentage of the current price to use to define the target order book depth.\"\n    BIDS_COUNT_DECS = \"Bids count: How many buy orders to create in the order book.\"\n    ASKS_COUNT_DECS = \"Asks count: How many sell orders to create in the order book.\"\n    REFERENCE_EXCHANGE_DESC = (\n        f\"Reference exchange. Used as the price source to create the order book's orders from. \"\n        f\"This exchange need to have a trading market for the selected traded pair. Example: \\\"binance\\\". \"\n        f\"Use \\\"{LOCAL_EXCHANGE_PRICE}\\\" to use the current exchange price.\"\n    )\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.UI.user_input(\n            self.MIN_SPREAD, commons_enums.UserInputTypes.FLOAT, 2, inputs,\n            min_val=0, max_val=int(order_book_distribution.TARGET_CUMULATED_VOLUME_PERCENT * 2) ,\n            other_schema_values={\"exclusiveMinimum\": True, \"exclusiveMaximum\": True}, title=self.MIN_SPREAD_DESC\n        )\n        self.UI.user_input(\n            self.MAX_SPREAD, commons_enums.UserInputTypes.FLOAT, 6, inputs,\n            min_val=0, max_val=200, other_schema_values={\"exclusiveMinimum\": True, \"exclusiveMaximum\": True},\n            title=self.MAX_SPREAD_DESC,\n        )\n        self.UI.user_input(\n            self.BIDS_COUNT, commons_enums.UserInputTypes.INT, 5, inputs,\n            min_val=0, max_val=order_book_distribution.MAX_HANDLED_BIDS_ORDERS,\n            other_schema_values={\"exclusiveMinimum\": True, \"exclusiveMaximum\": False}, title=self.BIDS_COUNT_DECS,\n        )\n        self.UI.user_input(\n            self.ASKS_COUNT, commons_enums.UserInputTypes.INT, 5, inputs,\n            min_val=0, max_val=order_book_distribution.MAX_HANDLED_ASKS_ORDERS,\n            other_schema_values={\"exclusiveMinimum\": True, \"exclusiveMaximum\": False}, title=self.ASKS_COUNT_DECS,\n        )\n        self.UI.user_input(\n            self.REFERENCE_EXCHANGE, commons_enums.UserInputTypes.TEXT,\n            \"binance\", inputs,\n            other_schema_values={\"inputAttributes\": {\"placeholder\": \"binance\"}},\n            title=self.REFERENCE_EXCHANGE_DESC\n        )\n\n    def get_current_state(self) -> (str, float):\n        order = self.producers[0].get_market_making_orders() if self.producers else []\n        bids = [o for o in order if o.side == trading_enums.TradeOrderSide.SELL]\n        asks = [o for o in order if o.side == trading_enums.TradeOrderSide.BUY]\n        if len(bids) > len(asks):\n            state = trading_enums.EvaluatorStates.LONG\n        elif len(bids) < len(asks):\n            state = trading_enums.EvaluatorStates.SHORT\n        else:\n            state = trading_enums.EvaluatorStates.NEUTRAL\n        bid_volume = sum(o.total_cost for o in bids)\n        ask_volume = sum(o.origin_quantity for o in asks)\n        symbol = symbol_util.parse_symbol(self.symbol)\n        return (\n            state.name,\n            f\"{bid_volume} {symbol.quote} in {len(bids)} bids, {ask_volume} {symbol.base} in {len(asks)} asks\"\n        )\n\n    def get_mode_producer_classes(self) -> list:\n        return [MarketMakingTradingModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [MarketMakingTradingModeConsumer]\n\n    @classmethod\n    async def get_forced_updater_channels(\n        cls, \n        exchange_manager: trading_exchanges.ExchangeManager,\n        tentacles_setup_config: tm_configuration.TentaclesSetupConfiguration, \n        trading_config: typing.Optional[dict]\n    ) -> set[trading_exchanges.ChannelSpecs]:\n        return set([\n            trading_exchanges.ChannelSpecs(\n                channel=trading_constants.TICKER_CHANNEL,\n            ),\n            trading_exchanges.ChannelSpecs(\n                channel=trading_constants.TRADES_CHANNEL,\n            )\n        ])\n\n    @classmethod\n    def get_is_trading_on_exchange(cls, exchange_name, tentacles_setup_config) -> bool:\n        \"\"\"\n        returns True if exchange_name is trading exchange or the hedging exchange\n        \"\"\"\n        return cls.has_trading_exchange_configuration(\n            exchange_name, octobot_tentacles_manager.api.get_tentacle_config(tentacles_setup_config, cls)\n        )\n\n    @classmethod\n    def get_is_using_trading_mode_on_exchange(cls, exchange_name, tentacles_setup_config) -> bool:\n        \"\"\"\n        returns True if exchange_name is a trading exchange that is not the hedging exchange\n        \"\"\"\n        return cls.has_trading_exchange_configuration(\n            exchange_name, octobot_tentacles_manager.api.get_tentacle_config(tentacles_setup_config, cls)\n        )\n\n    @classmethod\n    def has_trading_exchange_configuration(cls, exchange_name, tentacle_config: dict):\n        pairs_settings_for_exchange = cls.get_pair_settings_for_exchange(exchange_name, tentacle_config)\n        # trade on this exchange if there is at least a pair config for this exchange\n        return bool(pairs_settings_for_exchange)\n\n    @classmethod\n    def get_pair_settings_for_exchange(cls, target_exchange_name, tentacle_config) -> list:\n        if cls.is_exchange_compatible_pair_setting(tentacle_config, target_exchange_name):\n            return [tentacle_config]\n        return []\n\n    def get_pair_settings(self) -> list:\n        if self.is_exchange_compatible_pair_setting(self.trading_config, self.exchange_manager.exchange_name):\n            return [self.trading_config]\n        return []\n\n    @classmethod\n    def is_exchange_compatible_pair_setting(cls, trading_config: dict, target_exchange_name: str) -> bool:\n        return (\n            trading_config[cls.REFERENCE_EXCHANGE] != target_exchange_name\n        )\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n    @staticmethod\n    def is_backtestable():\n        return False\n\n    @classmethod\n    def is_ignoring_cancelled_orders_trades(cls) -> bool:\n        return True\n\n    async def create_consumers(self) -> list:\n        consumers = await super().create_consumers()\n        # order consumer: filter by symbol not be triggered only on this symbol's orders\n        order_consumer = await exchanges_channel.get_chan(trading_personal_data.OrdersChannel.get_name(),\n                                                          self.exchange_manager.id).new_consumer(\n            self._order_notification_callback,\n            symbol=self.symbol\n        )\n        return consumers + [order_consumer]\n\n    async def _order_notification_callback(\n        self, exchange, exchange_id, cryptocurrency, symbol, order, update_type, is_from_bot\n    ):\n        if (\n            order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] == trading_enums.OrderStatus.FILLED.value\n            and order[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] in (\n                trading_enums.TradeOrderType.LIMIT.value\n            )\n        ):\n            await self.producers[0].order_filled_callback(order)\n\n    def set_default_config(self):\n        raise RuntimeError(f\"Impossible to start {self.get_name()} without a valid configuration file.\")\n\n    @classmethod\n    def get_order_book_distribution(cls, pair_config: dict) -> order_book_distribution.OrderBookDistribution:\n        try:\n            min_spread = decimal.Decimal(str(pair_config[cls.MIN_SPREAD] / 100))\n            max_spread = decimal.Decimal(str(pair_config[cls.MAX_SPREAD] / 100))\n            bids_count = int(pair_config[cls.BIDS_COUNT])\n            asks_count = int(pair_config[cls.ASKS_COUNT])\n            return order_book_distribution.OrderBookDistribution(\n                bids_count, asks_count, min_spread, max_spread,\n            )\n        except TypeError as err:\n            raise ValueError(f\"Invalid config value: {err}\") from err\n\n\nclass MarketMakingTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    ORDER_ACTIONS_PLAN_KEY = \"order_actions_plan\"\n    CURRENT_PRICE_KEY = \"current_price\"\n    SYMBOL_MARKET_KEY = \"symbol_market\"\n\n    def skip_portfolio_available_check_before_creating_orders(self) -> bool:\n        \"\"\"\n        When returning true, will skip portfolio available funds check\n        before calling self.create_new_orders().\n        Override if necessary\n        \"\"\"\n        # will cancel orders and free funds if necessary\n        return True\n\n    async def create_new_orders(self, symbol, final_note, state, **kwargs):\n        # use dict default getter: can't afford missing data\n        data = kwargs[self.CREATE_ORDER_DATA_PARAM]\n        order_actions_plan: OrdersUpdatePlan = data[self.ORDER_ACTIONS_PLAN_KEY]\n        current_price = data[self.CURRENT_PRICE_KEY]\n        symbol_market = data[self.SYMBOL_MARKET_KEY]\n        try:\n            if order_actions_plan.cancelled:\n                # any plan can be cancelled if it did not start processing\n                self.logger.info(f\"Cancelling {str(order_actions_plan)} action plan processing: plan did not start\")\n                return []\n            else:\n                self.logger.info(f\"Starting {str(order_actions_plan)} action plan processing\")\n                return await self._process_plan(order_actions_plan, current_price, symbol_market)\n        finally:\n            order_actions_plan.processed.set()\n\n    async def _process_plan(self, order_actions_plan: OrdersUpdatePlan, current_price, symbol_market):\n        created_orders = []\n        cancelled_orders = []\n        processed_actions = {}\n        skipped_actions = {}\n        scheduled_actions = collections.deque(order_actions_plan.order_actions)\n\n        while scheduled_actions:\n            action = scheduled_actions.popleft()\n            try:\n                if (\n                    (order_actions_plan.cancelled and order_actions_plan.cancellable) or order_actions_plan.force_cancelled\n                ):\n                    actions_class = action.__class__.__name__\n                    self.logger.debug(\n                        f\"{self.trading_mode.symbol} {self.exchange_manager.exchange_name} \"\n                        f\"order actions cancelled, skipping {actions_class} action.\"\n                    )\n                    if actions_class not in skipped_actions:\n                        skipped_actions[actions_class] = 1\n                    else:\n                        skipped_actions[actions_class] += 1\n                else:\n                    await self._process_action(\n                        action, current_price, symbol_market,\n                        processed_actions, created_orders, cancelled_orders\n                    )\n            except Exception as err:\n                self.logger.exception(err, True, f\"Error when processing {action}: {err}\")\n\n        self._log_actions_report(\n            order_actions_plan, processed_actions, skipped_actions, created_orders, cancelled_orders\n        )\n        return created_orders\n\n    def _log_actions_report(\n        self, order_actions_plan, processed_actions, skipped_actions, created_orders, cancelled_orders\n    ):\n        skipped_actions_str = f\", skipped actions: {skipped_actions}\" if skipped_actions else ''\n        create_actions = processed_actions.get(CreateOrderAction.__name__, 0)\n        cancel_actions = processed_actions.get(CancelOrderAction.__name__, 0)\n        self.logger.info(\n            f\"Completed {self.trading_mode.symbol} [{self.exchange_manager.exchange_name}] \"\n            f\"{cancel_actions + create_actions}/{len(order_actions_plan.order_actions)} \"\n            f\"order actions: {len(created_orders)}/{create_actions} created orders, \"\n            f\"{len(cancelled_orders)}/{cancel_actions} cancelled orders{skipped_actions_str}.\"\n        )\n\n    async def _process_action(\n        self, action: OrderAction, current_price, symbol_market,\n        processed_actions: dict, created_orders: list, cancelled_orders: list,\n        **kwargs,\n    ):\n        actions_class = action.__class__.__name__\n        if isinstance(action, CreateOrderAction):\n            created_orders += (\n                await self.create_order(action.order_data, current_price, symbol_market, **kwargs)\n            )\n        elif isinstance(action, CancelOrderAction):\n            if action.order.is_open():\n                try:\n                    await self.trading_mode.cancel_order(action.order)\n                    cancelled_orders.append(action.order.order_id)\n                except trading_errors.UnexpectedExchangeSideOrderStateError as err:\n                    self.logger.warning(f\"Skipped order cancel: {err}, order: {str(action.order)}\")\n                except trading_errors.OrderCancelError as err:\n                    self.logger.warning(\n                        f\"Error when cancelling order, considering order as closed. Error: {err}, \"\n                        f\"order: {str(action.order)}\"\n                    )\n            else:\n                self.logger.info(\n                    f\"{self.trading_mode.symbol} {self.exchange_manager.exchange_name} ignored cancel order \"\n                    f\"action: Order is not open anymore. Order: {str(action.order)}\"\n                )\n        else:\n            raise NotImplementedError(\n                f\"{self.trading_mode.symbol} {self.exchange_manager.exchange_name} {action} is not supported\"\n            )\n        if actions_class not in processed_actions:\n            processed_actions[actions_class] = 1\n        else:\n            processed_actions[actions_class] += 1\n\n    async def create_order(self, order_data, current_price, symbol_market, **kwargs):\n        created_order = None\n        currency, market = symbol_util.parse_symbol(order_data.symbol).base_and_quote()\n        try:\n            base_available = trading_api.get_portfolio_currency(self.exchange_manager, currency).available\n            quote_available = trading_api.get_portfolio_currency(self.exchange_manager, market).available\n            selling = order_data.side == trading_enums.TradeOrderSide.SELL\n            quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                self.exchange_manager, order_data.symbol,\n                trading_enums.TraderOrderType.SELL_LIMIT if selling else trading_enums.TraderOrderType.BUY_LIMIT,\n                order_data.quantity, order_data.price, order_data.side,\n            )\n            orders_quantity_and_price = trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                quantity,\n                order_data.price,\n                symbol_market\n            )\n            if orders_quantity_and_price:\n                if len(orders_quantity_and_price) > 1:\n                    self.logger.error(\n                        f\"Orders to create are too large and have to be split. This is not supported. \"\n                        f\"Only creating the 1s order.\"\n                    )\n                    orders_quantity_and_price = orders_quantity_and_price[:1]\n                for order_quantity, order_price in orders_quantity_and_price:\n                    order_desc = (\n                        f\"{order_data.symbol} {order_data.side.value} [{self.exchange_manager.exchange_name}] order \"\n                        f\"creation of {order_quantity} at {float(order_price)}\"\n                    )\n                    should_skip, skip_message = self._should_skip(\n                        selling, base_available, quote_available, order_quantity, order_price, order_desc, currency,\n                        market, **kwargs\n                    )\n                    if should_skip:\n                        self.logger.warning(f\"Skipping {skip_message}\")\n                        return []\n                    order_type = trading_enums.TraderOrderType.SELL_LIMIT if selling \\\n                        else trading_enums.TraderOrderType.BUY_LIMIT\n                    current_order = trading_personal_data.create_order_instance(\n                        trader=self.exchange_manager.trader,\n                        order_type=order_type,\n                        symbol=order_data.symbol,\n                        current_price=current_price,\n                        quantity=order_quantity,\n                        price=order_price,\n                    )\n                    # disable instant fill to avoid looping order fill in simulator\n                    current_order.allow_instant_fill = False\n                    created_order = await self.trading_mode.create_order(current_order)\n            if not created_order:\n                self.logger.warning(\n                    f\"No order created for {order_data} (quantity: {quantity}): \"\n                    f\"incompatible with exchange minimum rules. \"\n                    f\"Limits: {symbol_market[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value]}\"\n                )\n        except (SkippedAction, trading_errors.MissingFunds):\n            raise\n        except Exception as e:\n            self.logger.error(f\"Failed to create order : {e}. Order: {order_data}\")\n            return []\n        return [] if created_order is None else [created_order]\n\n    def _should_skip(\n        self, selling, base_available, quote_available, order_quantity, order_price,\n        order_desc, currency, market, **kwargs\n    ):\n        skip_message = \"\"\n        if selling:\n            if base_available < order_quantity:\n                skip_message = (\n                    f\"{order_desc}: \"\n                    f\"not enough {currency}: available: {base_available}, required: {order_quantity}\"\n                )\n        elif quote_available < order_quantity * order_price:\n            skip_message = (\n                f\"Skipping {order_desc}: not enough {market}: available: {quote_available}, \"\n                f\"required: {order_quantity * order_price}\"\n            )\n        return bool(skip_message), skip_message\n\n\nclass MarketMakingTradingModeProducer(trading_modes.AbstractTradingModeProducer):\n    PRICE_FETCHING_TIMEOUT = 60\n    ORDER_ACTION_TIMEOUT = 20\n    INIT_RETRY_TIMER = 5\n    REFERENCE_PRICE_INIT_DELAY = 60 # allow 60s before logging missing reference prices as error\n    ORDERS_DESC = \"market making\"\n\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        # no state for this evaluator: always neutral\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n\n        # config\n        self.symbol: str = trading_mode.symbol\n        self.order_book_distribution: order_book_distribution.OrderBookDistribution = None\n        self.reference_price = reference_price_import.PriceSource\n        self.replace_whole_book_distance_threshold: float = 0.5\n\n        self.symbol_trading_config: dict = None\n        self.healthy = False\n        self.subscribed_channel_specs_by_exchange_id: dict[str, set[trading_exchanges.ChannelSpecs]] = {}\n        self.is_first_execution: bool = True\n        self._started_at: float = 0\n        self._last_error_at: float = 0\n        self.latest_actions_plan: OrdersUpdatePlan = None\n        self.last_target_buy_orders_count: int = 0\n        self.last_target_sell_orders_count: int = 0\n\n        try:\n            self._load_symbol_trading_config()\n        except KeyError as e:\n            error_message = f\"Impossible to start {self.ORDERS_DESC} orders for {self.symbol}: missing \" \\\n                            f\"configuration in trading mode config file. \"\n            self.logger.exception(e, True, error_message)\n            return\n        if self.symbol_trading_config is None:\n            return\n        self.read_config()\n\n        self.logger.debug(f\"Loaded healthy config for {self.symbol}\")\n        self.healthy = True\n\n    def _load_symbol_trading_config(self) -> bool:\n        self.symbol_trading_config = self.trading_mode.get_pair_settings()[0]\n        return True\n\n    def read_config(self):\n        self.order_book_distribution = self.trading_mode.get_order_book_distribution(self.symbol_trading_config)\n        self.reference_price = reference_price_import.PriceSource(\n            self.symbol_trading_config[self.trading_mode.REFERENCE_EXCHANGE],\n            self.symbol\n        )\n        if len(self.exchange_manager.exchange_config.traded_symbols) > 1:\n            error = (\n                f\"Multiple trading pair is not supported on {self.trading_mode.get_name()}. \"\n                f\"Please select only one trading pair in configuration.\"\n            )\n            asyncio.create_task(\n                self.sent_once_critical_notification(\n                    \"Configuration issue\",\n                    error\n                )\n            )\n            raise ValueError(error)\n        enabled_exchanges = trading_exchanges.get_enabled_exchanges(self.exchange_manager.config)\n        if (\n            self.reference_price.exchange != self.trading_mode.LOCAL_EXCHANGE_PRICE and\n            self.reference_price.exchange not in enabled_exchanges\n        ):\n            error = (\n                f\"Reference exchange is missing from configuration. Please add {self.reference_price.exchange} to \"\n                f\"configured exchanges or use another reference exchange.\"\n            )\n            asyncio.create_task(\n                self.sent_once_critical_notification(\n                    \"Configuration issue\",\n                    error\n                )\n            )\n            raise ValueError(error)\n\n    async def start(self) -> None:\n        await super().start()\n        if self.healthy:\n            self.logger.debug(f\"Initializing orders creation\")\n            await self._ensure_market_making_orders_and_reschedule()\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        # nothing to do: this is not a strategy related trading mode\n        pass\n\n    def _schedule_order_refresh(self):\n        # schedule order creation / health check\n        asyncio.create_task(self._ensure_market_making_orders_and_reschedule())\n\n    async def _ensure_market_making_orders_and_reschedule(self):\n        if self.should_stop:\n            return\n        can_create_orders = (\n            not trading_api.get_is_backtesting(self.exchange_manager)\n            or trading_api.is_mark_price_initialized(self.exchange_manager, symbol=self.symbol)\n        ) and (\n            trading_api.get_portfolio(self.exchange_manager) != {}\n            or trading_api.is_trader_simulated(self.exchange_manager)\n        )\n        if can_create_orders:\n            try:\n                if not await self._ensure_market_making_orders(\n                    \"initial trigger\" if self.is_first_execution else \"periodic trigger\"\n                ):\n                    can_create_orders = False\n            except asyncio.TimeoutError:\n                can_create_orders = False\n        if not self.should_stop:\n            await self._reschedule_if_necessary(can_create_orders)\n\n    async def _reschedule_if_necessary(self, can_create_orders: bool):\n        if not can_create_orders:\n            self.logger.info(\n                f\"Can't yet create initialize orders for {self.symbol}, retrying in {self.INIT_RETRY_TIMER} seconds\"\n            )\n            # avoid spamming retries when price is not available\n            self.scheduled_health_check = asyncio.get_event_loop().call_later(\n                self.INIT_RETRY_TIMER,\n                self._schedule_order_refresh\n            )\n\n    async def _ensure_market_making_orders(self, trigger_source: str):\n        # can be called:\n        #   - on initialization\n        #   - when price moves beyond spread\n        #   - when orders are filled\n        _, _, _, current_price, symbol_market = await trading_personal_data.get_pre_order_data(\n            self.exchange_manager,\n            symbol=self.symbol,\n            timeout=self.PRICE_FETCHING_TIMEOUT\n        )\n        return await self.create_state(current_price, symbol_market, trigger_source, False)\n\n    async def create_state(self, current_price, symbol_market, trigger_source: str, force_full_refresh: bool):\n        if current_price is not None:\n            async with self.trading_mode_trigger(skip_health_check=True):\n                if self.exchange_manager.trader.is_enabled:\n                    try:\n                        if await self._handle_market_making_orders(\n                            current_price, symbol_market, trigger_source, force_full_refresh\n                        ):\n                            self.is_first_execution = False\n                            self._started_at = self.exchange_manager.exchange.get_exchange_current_time()\n                            return True\n                    except ValueError as err:\n                        if self._last_error_at <= self._started_at:\n                            # only log full exception every 1st time it occurs then use warnings to avoid flooding\n                            # when on websockets\n                            self.logger.exception(\n                                err, True, f\"Unexpected error when starting {self.symbol} trading mode: {err}\"\n                            )\n                        else:\n                            self.logger.warning(f\"Skipped {self.symbol} orders update: {err}\")\n                        self._last_error_at = self.exchange_manager.exchange.get_exchange_current_time()\n                        if \"Missing volume\" not in str(err):\n                            # config error: should not happen, in this case, return true to skip auto reschedule\n                            await self.sent_once_critical_notification(\n                                \"Configuration issue\",\n                                f\"Impossible to start {self.symbol} market making \"\n                                f\"on {self.exchange_manager.exchange_name}: {err}\"\n                            )\n                        return True\n        return False\n\n    def _is_previous_plan_still_processing(self) -> bool:\n        return self.latest_actions_plan is not None and not self.latest_actions_plan.processed.is_set()\n\n    async def _handle_market_making_orders(\n        self, current_price, symbol_market, trigger_source: str, force_full_refresh: bool\n    ):\n        # 1. get price from external source\n        reference_price = await self._get_reference_price()\n        if not reference_price:\n            method = self.logger.info if self.is_first_execution else self.logger.error\n            method(\n                f\"Skipped trigger: can't compute {self.symbol} reference price for\"\n                f\" {self.exchange_manager.exchange_name}: {reference_price=}\"\n            )\n            return False\n        daily_base_volume, daily_quote_volume = self._get_daily_volume(reference_price)\n        if not all(v and not v.is_nan() for v in (daily_base_volume, daily_quote_volume)):\n            method = self.logger.info if self.is_first_execution else self.logger.error\n            method(\n                f\"Skipped trigger: can't compute {self.symbol} daily volume for\"\n                f\" {self.exchange_manager.exchange_name}: {daily_base_volume=} {daily_quote_volume=}\"\n            )\n            return False\n        base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        self.logger.info(\n            f\"Trigger for {self.symbol} on {self.exchange_manager.exchange_name}. Ref price: {float(reference_price)} \"\n            f\"daily {base} vol: {octobot_commons.pretty_printer.get_min_string_from_number(daily_base_volume)} \"\n            f\"daily {quote} vol: {octobot_commons.pretty_printer.get_min_string_from_number(daily_quote_volume)} \"\n            f\"[trigger source: {trigger_source}]\"\n        )\n        open_orders = self.get_market_making_orders()\n        require_data_refresh = False\n        if self._is_previous_plan_still_processing():\n            # if previous plan is still processing but being cancelled: skip call (another one is waiting for cancel)\n            skip_exec = self.latest_actions_plan.cancelled\n            # if previous plan is still processing and not being cancelled: check if cancel is required\n            if not self.latest_actions_plan.cancelled:\n                # only cancel latest plan if outdated and still processing, otherwise ignore signal\n                previous_plan_orders = [\n                    action.order_data\n                    for action in self.latest_actions_plan.order_actions\n                    if isinstance(action, CreateOrderAction)\n                ]\n                previous_plan_cancelled_orders = [\n                    action.order\n                    for action in self.latest_actions_plan.order_actions\n                    if isinstance(action, CancelOrderAction)\n                ]\n                remaining_open_orders = [\n                    order\n                    for order in open_orders\n                    if order not in previous_plan_cancelled_orders\n                ]\n                if self._get_orders_to_cancel(previous_plan_orders + remaining_open_orders, reference_price):\n                    # cancel previous plan\n                    self.latest_actions_plan.cancelled = True\n                    if self.latest_actions_plan.cancellable:\n                        self.logger.debug(\n                            f\"Cancelling previous plan after {reference_price} {self.symbol} price update for \"\n                            f\"{self.exchange_manager.exchange_name}: orders are outdated \"\n                            f\"[trigger source: {trigger_source}].\"\n                        )\n                    else:\n                        self.logger.debug(\n                            f\"Waiting for non-cancellable action plan to complete: {reference_price} {self.symbol} \"\n                            f\"price update for {self.exchange_manager.exchange_name}: orders are outdated \"\n                            f\"[trigger source: {trigger_source}].\"\n                        )\n                    try:\n                        waiting_plan = self.latest_actions_plan\n                        await asyncio.wait_for(waiting_plan.processed.wait(), self.ORDER_ACTION_TIMEOUT)\n                        if (\n                            self.latest_actions_plan is not waiting_plan\n                            and not self.latest_actions_plan.processed.is_set()\n                        ):\n                            # plan just changed, skip this update\n                            self.logger.debug(\n                                f\"Skip {self.symbol} {self.exchange_manager.exchange_name} plan execution: a new\"\n                                f\"plan is already being executed [trigger source: {trigger_source}]\"\n                            )\n                            skip_exec = True\n                        else:\n                            self.logger.debug(\n                                f\"Continuing {reference_price} {self.symbol} after latest action plan cancel \"\n                                f\"[trigger source: {trigger_source}]\"\n                            )\n                    except asyncio.TimeoutError:\n                        # don't continue, next refresh will take care of it\n                        self.logger.debug(\n                            f\"Timeout when waiting for {reference_price} {self.symbol} latest action plan: \"\n                            f\"{str(self.latest_actions_plan)} [trigger source: {trigger_source}]\"\n                        )\n                        skip_exec = True\n                    finally:\n                        require_data_refresh = True\n                else:\n                    skip_exec = True\n            if skip_exec:\n                # let previous plan execute, ignore signal\n                self.logger.debug(\n                    f\"Ignored {reference_price} {self.symbol} price update for {self.exchange_manager.exchange_name} \"\n                    f\"while previous orders plan is still processing [trigger source: {trigger_source}]\"\n                )\n                return False\n\n        if require_data_refresh:\n            # update reference price in case it changed\n            reference_price = await self._get_reference_price()\n            if not reference_price:\n                self.logger.error(\n                    f\"Can't compute reference price for {self.exchange_manager.exchange_name}: after waiting \"\n                    f\"for previous plan processing: {reference_price=}\"\n                )\n                return False\n            # update open orders in case it changed after waiting\n            open_orders = self.get_market_making_orders()\n\n        sorted_orders = self._sort_orders(open_orders)\n        available_base, available_quote = self._get_available_funds()\n        theoretically_available_base, theoretically_available_quote = (\n            self._get_all_theoretically_available_funds(open_orders)\n        )\n        self.logger.debug(\n            f\"MM available {self.symbol} funds: {base}: {float(available_base)} {quote}: {float(available_quote)}\"\n        )\n\n        # 2. cancel outdated orders\n        outdated_orders = self._get_orders_to_cancel(sorted_orders, reference_price)\n        if outdated_orders:\n            self.logger.info(\n                f\"{len(outdated_orders)} outdated orders for {self.symbol} on {self.exchange_manager.exchange_name} (trigger_source: {trigger_source}): \"\n                f\"{[str(o) for o in outdated_orders]}\"\n            )\n\n        # get ideal distribution\n        ideal_distribution = self.order_book_distribution.compute_distribution(\n            reference_price,\n            daily_base_volume, daily_quote_volume,\n            symbol_market,\n            available_base=theoretically_available_base, available_quote=theoretically_available_quote,\n        )\n        # update last target orders count\n        self.last_target_sell_orders_count = len(ideal_distribution.asks)\n        self.last_target_buy_orders_count = len(ideal_distribution.bids)\n        cancelled_orders = created_orders = []\n        missing_all_orders_sides = []\n        try:\n            if force_full_refresh:\n                raise order_book_distribution.FullBookRebalanceRequired(\"Forced full refresh\")\n            book_orders_after_swaps, cancelled_orders, created_orders = (\n                self._get_swapped_book_orders(\n                    sorted_orders, outdated_orders, available_base, available_quote, reference_price,\n                    ideal_distribution, daily_base_volume, daily_quote_volume\n                )\n            )\n            is_spread_according_to_config = ideal_distribution.is_spread_according_to_config(\n                book_orders_after_swaps, open_orders\n            )\n            # Compute distance from distribution.\n            # Warning: filled orders result in more funds being available, which can create full order book rebalance as\n            # distance from ideal would become too large. This can happen when the order book depth is far from the required\n            # value.\n            distance_from_ideal_after_swaps = ideal_distribution.get_shape_distance_from(\n                book_orders_after_swaps, theoretically_available_base, theoretically_available_quote,\n                reference_price, daily_base_volume, daily_quote_volume, trigger_source\n            )\n            can_just_replace_a_few_orders = is_spread_according_to_config and (\n                distance_from_ideal_after_swaps < self.replace_whole_book_distance_threshold\n            )\n        except order_book_distribution.MissingOrderException as err:\n            orders = []\n            if isinstance(err, order_book_distribution.MissingAllOrders):\n                orders = ideal_distribution.asks + ideal_distribution.bids\n                missing_all_orders_sides.extend((trading_enums.TradeOrderSide.BUY, trading_enums.TradeOrderSide.SELL))\n            elif isinstance(err, order_book_distribution.MissingAllAsks):\n                orders = ideal_distribution.asks\n                missing_all_orders_sides.append(trading_enums.TradeOrderSide.SELL)\n            elif isinstance(err, order_book_distribution.MissingAllBids):\n                orders = ideal_distribution.bids\n                missing_all_orders_sides.append(trading_enums.TradeOrderSide.BUY)\n            # no open order but can create some if the total amount of orders is > 0\n            # => means we have the funds to create those orders, we should create them\n            self.logger.info(\n                f\"Missing orders on {2 if isinstance(err, order_book_distribution.MissingAllOrders) else 1} \"\n                f\"side: {err.__class__.__name__}\"\n            )\n            is_spread_according_to_config = False\n            distance_from_ideal_after_swaps = trading_constants.ONE\n            can_just_replace_a_few_orders = not sum(o.amount for o in orders) > trading_constants.ZERO\n        except order_book_distribution.FullBookRebalanceRequired as err:\n            cancelled_orders = created_orders = []\n            is_spread_according_to_config = True\n            distance_from_ideal_after_swaps = trading_constants.ONE\n            can_just_replace_a_few_orders = False\n            self.logger.info(f\"Scheduling full order book refresh: {err}\")\n\n        if can_just_replace_a_few_orders:\n            if sum(o.amount for o in created_orders) <= trading_constants.ZERO:\n                # no order can be created (not enough funds)\n                created_orders = []\n                await self._send_missing_funds_critical_notification(missing_all_orders_sides)\n                self.logger.info(\n                    f\"No order to create (order amounts is 0), leaving book as is [trigger source: {trigger_source}]\"\n                )\n            elif not self._can_create_all_order(created_orders, symbol_market):\n                # if orders can't be created (because too small for example), then recreate the whole book to\n                # create all orders\n                self.logger.warning(\n                    f\"Missing funds: few orders can't be created, resizing book instead [trigger source: {trigger_source}]\"\n                )\n                can_just_replace_a_few_orders = False\n\n        if can_just_replace_a_few_orders:\n            # A. Threshold is not met\n            if not (outdated_orders or cancelled_orders or created_orders):\n                # A.1: no order to replace, nothing to do\n                self.logger.debug(\n                    f\"{self.symbol} {self.exchange_manager.exchange_name} orders are up to date \"\n                    f\"[trigger source: {trigger_source}]\"\n                )\n                return True\n            else:\n                # A.2: orders are just created to fill the order book\n                self.logger.info(\n                    f\"Replacing {self.symbol} {self.exchange_manager.exchange_name} missing orders: \"\n                    f\"{len(outdated_orders)} outdated orders, {len(cancelled_orders)} cancelled_orders, \"\n                    f\"{len(created_orders)} created_orders spread conform to config: {is_spread_according_to_config} \"\n                    f\"[trigger source: {trigger_source}]\"\n                )\n                order_actions_plan = self._get_create_missing_orders_plan(\n                    outdated_orders, cancelled_orders, created_orders\n                )\n        else:\n            # B. A full order book replacement is required if new orders can fix the issue\n            if len(missing_all_orders_sides) != 1 or (\n                len(missing_all_orders_sides) == 1 and ideal_distribution.can_create_at_least_one_order(\n                    missing_all_orders_sides, symbol_market\n                )\n            ):\n                # B.1: Orders should and can be replaced: replaced them one by one\n                self.logger.info(\n                    f\"Re-creating the whole {self.symbol} {self.exchange_manager.exchange_name} order book: book is too \"\n                    f\"different from configuration (distance: {distance_from_ideal_after_swaps}) \"\n                    f\"[trigger source: {trigger_source}]\"\n                )\n            else:\n                # B.2: Orders can't be replaced: they are not following exchange requirements: skip them\n                for side in missing_all_orders_sides:\n                    # filter out orders\n                    created_orders = [\n                        order for order in created_orders\n                        if order.side != side\n                    ]\n                    # remove filtered orders from ideal_distribution in case an action plan gets created\n                    if side == trading_enums.TradeOrderSide.BUY:\n                        ideal_distribution.bids.clear()\n                    else:\n                        ideal_distribution.asks.clear()\n                skip_iteration = not created_orders and not cancelled_orders\n                error_details = await self._send_missing_funds_critical_notification(missing_all_orders_sides)\n                self.logger.warning(f\"{'Skipped iteration: ' if skip_iteration else ''}{error_details}\")\n                if skip_iteration:\n                    # all the orders to create can't actually be created and there is nothing to replace: nothing to do\n                    return True\n            # create action plan from ideal distribution\n            order_actions_plan = self._get_replace_full_book_plan(\n                outdated_orders, sorted_orders, ideal_distribution\n            )\n\n        order_actions_plan.trigger_source = trigger_source\n        # 4. push orders creation and cancel plan\n        await self._schedule_order_actions(order_actions_plan, current_price, symbol_market)\n        return True\n\n    async def _send_missing_funds_critical_notification(self, missing_all_orders_sides) -> str:\n        base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        required_funds = []\n        for side in missing_all_orders_sides:\n            if side == trading_enums.TradeOrderSide.BUY:\n                required_funds.append(quote)\n            else:\n                required_funds.append(base)\n        if required_funds:\n            missing_funds = ' and '.join(required_funds)\n            error_details = (\n                f\"Impossible to create {self.symbol} {' and '.join([s.value for s in missing_all_orders_sides])} \"\n                f\"orders: missing available funds to comply with {self.exchange_manager.exchange_name} \"\n                f\"minimal order size rules. Additional {missing_funds} required.\"\n            )\n            await self.sent_once_critical_notification(f\"More {missing_funds} required\", error_details)\n            return error_details\n        return \"\"\n\n    def _can_create_all_order(self, created_orders: list[order_book_distribution.BookOrderData], symbol_market):\n        for order in created_orders:\n            if not trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                order.amount,\n                order.price,\n                symbol_market\n            ):\n                self.logger.info(f\"{order} can't be created: {order.amount=} or {order.price=} are too small\")\n                return False\n        return True\n\n    def _get_swapped_book_orders(\n        self, sorted_orders, outdated_orders, available_base,\n        available_quote, reference_price, ideal_distribution,\n        daily_base_volume, daily_quote_volume\n    ):\n        remaining_orders = [o for o in sorted_orders if o not in outdated_orders]\n        remaining_orders_data = [\n            order_book_distribution.BookOrderData(\n                o.origin_price,\n                o.origin_quantity,\n                o.side,\n            )\n            for o in remaining_orders\n        ]\n        remaining_order_prices = [o.price for o in remaining_orders_data]\n        updated_book_orders: list[order_book_distribution.BookOrderData] = (\n            ideal_distribution.infer_full_order_data_after_swaps(\n                remaining_orders_data, outdated_orders, available_base,\n                available_quote, reference_price, daily_base_volume, daily_quote_volume\n            )\n        )\n        created_orders = [\n            order\n            for order in updated_book_orders\n            if order.price not in remaining_order_prices\n        ]\n        updated_book_order_prices = [o.price for o in updated_book_orders]\n        cancelled_orders = [\n            order\n            for order in remaining_orders\n            if order.origin_price not in updated_book_order_prices\n        ]\n        return updated_book_orders, cancelled_orders, created_orders\n\n    def _get_create_missing_orders_plan(\n        self,\n        outdated_orders: list[trading_personal_data.Order],\n        cancelled_orders: list[trading_personal_data.Order],\n        created_orders: list[order_book_distribution.BookOrderData]\n    ) -> OrdersUpdatePlan:\n        # 1. cancel outdated orders\n        orders_actions: list[OrderAction] = [CancelOrderAction(order) for order in outdated_orders]\n        # 2. replace orders\n        orders_actions += self._get_alternated_cancel_and_create_order_actions(\n            cancelled_orders, created_orders, False\n        )\n        return OrdersUpdatePlan(orders_actions)\n\n    def _get_replace_full_book_plan(\n        self,\n        outdated_orders: list[trading_personal_data.Order],\n        existing_orders: list[trading_personal_data.Order],\n        ideal_distribution: order_book_distribution.OrderBookDistribution\n    ) -> OrdersUpdatePlan:\n        # 1. cancel outdated orders\n        orders_actions: list[OrderAction] = [CancelOrderAction(order) for order in outdated_orders]\n        cancelled_orders = [o for o in existing_orders if o not in outdated_orders]\n        # 2. recreate orders\n        orders_actions += self._get_alternated_cancel_and_create_order_actions(\n            cancelled_orders, ideal_distribution.asks + ideal_distribution.bids, True\n        )\n        return OrdersUpdatePlan(orders_actions, cancellable=False)\n\n    def _get_alternated_cancel_and_create_order_actions(\n        self,\n        cancelled_orders: list[trading_personal_data.Order],\n        created_orders: list[order_book_distribution.BookOrderData],\n        cancel_closer_orders_first_from_second_cancel: bool,\n    ):\n        orders_actions: list[OrderAction] = []\n        cancelled_buy_orders, created_buy_orders, cancelled_sell_orders, created_sell_orders = \\\n            self._get_prioritized_orders(\n                cancelled_orders, created_orders, cancel_closer_orders_first_from_second_cancel\n            )\n        # alternate between cancel and create to \"move\" orders to their new price\n        for i in range(max(\n            len(cancelled_buy_orders), len(created_buy_orders), len(cancelled_sell_orders), len(created_sell_orders)\n        )):\n            if i < len(cancelled_buy_orders):\n                orders_actions.append(CancelOrderAction(cancelled_buy_orders[i]))\n            if i < len(cancelled_sell_orders):\n                orders_actions.append(CancelOrderAction(cancelled_sell_orders[i]))\n            if i < len(created_buy_orders):\n                orders_actions.append(CreateOrderAction.from_book_order_data(self.symbol, created_buy_orders[i]))\n            if i < len(created_sell_orders):\n                orders_actions.append(CreateOrderAction.from_book_order_data(self.symbol, created_sell_orders[i]))\n        return orders_actions\n\n    def _get_prioritized_orders(\n        self,\n        cancelled_orders: list[trading_personal_data.Order],\n        created_orders: list[order_book_distribution.BookOrderData],\n        cancel_closer_orders_first_from_second_cancel: bool,\n    ):\n        # 1st cancelled order is always the furthest from spread.\n        cancelled_buy_orders = sorted(\n            [o for o in cancelled_orders if o.side is trading_enums.TradeOrderSide.BUY],\n            key=lambda o: o.origin_price,  # lowest first\n        )\n        cancelled_sell_orders = sorted(\n            [o for o in cancelled_orders if o.side is trading_enums.TradeOrderSide.SELL],\n            key=lambda o: o.origin_price, reverse=True,  # highest first\n        )\n        if cancel_closer_orders_first_from_second_cancel:\n            # 2nd cancelled order onwards are either start from the spread or from the outer orders\n            if len(cancelled_buy_orders) > 1:\n                cancelled_buy_orders = [cancelled_buy_orders[0]] + list(reversed(cancelled_buy_orders[1:]))\n            if len(cancelled_sell_orders) > 1:\n                cancelled_sell_orders = [cancelled_sell_orders[0]] + list(reversed(cancelled_sell_orders[1:]))\n        created_buy_orders = order_book_distribution.get_sorted_sided_orders(\n            [o for o in created_orders if o.side is trading_enums.TradeOrderSide.BUY],\n            True  # highest first\n        )\n\n        created_sell_orders = order_book_distribution.get_sorted_sided_orders(\n            [o for o in created_orders if o.side is trading_enums.TradeOrderSide.SELL],\n            True  # lowest first\n        )\n        return (\n            cancelled_buy_orders, created_buy_orders, cancelled_sell_orders, created_sell_orders\n        )\n\n    def _get_orders_to_cancel(\n        self,\n        open_orders: list[typing.Union[trading_personal_data.Order, OrderData]],\n        reference_price: decimal.Decimal\n    ) -> list[trading_personal_data.Order]:\n        return [\n            order\n            for order in open_orders\n            if self._is_outdated(\n                order.origin_price if isinstance(order, trading_personal_data.Order) else order.price,\n                order.side,\n                reference_price\n            )\n        ]\n\n    def _is_outdated(\n        self, order_price: decimal.Decimal, side: trading_enums.TradeOrderSide, reference_price: decimal.Decimal\n    ) -> bool:\n        if side == trading_enums.TradeOrderSide.BUY:\n            return order_price > reference_price\n        return order_price < reference_price\n\n    def _sort_orders(self, open_orders: list) -> list:\n        \"\"\"\n        Sort orders from the closest to the farthest from spread starting with buy orders\n        \"\"\"\n        buy_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        return (\n            sorted(buy_orders, key=lambda o: o.origin_price, reverse=True)\n            + sorted(sell_orders, key=lambda o: o.origin_price)\n        )\n\n    @classmethod\n    def get_should_cancel_loaded_orders(cls):\n        return False\n\n    async def _schedule_order_actions(self, order_actions_plan: OrdersUpdatePlan, current_price, symbol_market):\n        self.logger.info(\n            f\"Scheduling {self.symbol} {self.exchange_manager.exchange_name} {str(order_actions_plan)} using \"\n            f\"current price: {current_price}\"\n        )\n        data = {\n            MarketMakingTradingModeConsumer.ORDER_ACTIONS_PLAN_KEY: order_actions_plan,\n            MarketMakingTradingModeConsumer.CURRENT_PRICE_KEY: current_price,\n            MarketMakingTradingModeConsumer.SYMBOL_MARKET_KEY: symbol_market,\n        }\n        self.latest_actions_plan = order_actions_plan\n        await self.submit_trading_evaluation(\n            cryptocurrency=self.trading_mode.cryptocurrency,\n            symbol=self.trading_mode.symbol,\n            time_frame=None,\n            state=trading_enums.EvaluatorStates.NEUTRAL,\n            data=data\n        )\n\n    def _get_orders_to_create(\n        self,\n        reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal, daily_quote_volume: decimal.Decimal,\n        available_base: decimal.Decimal, available_quote: decimal.Decimal,\n        symbol_market: dict\n    ) -> list[OrderData]:\n        orders = []\n        distribution = self.order_book_distribution.compute_distribution(\n            reference_price,\n            daily_base_volume, daily_quote_volume,\n            symbol_market,\n            available_base=available_base, available_quote=available_quote,\n        )\n        asks = collections.deque(\n            OrderData(\n                trading_enums.TradeOrderSide.SELL,\n                book_order.amount,\n                book_order.price,\n                self.symbol,\n\n            )\n            for book_order in distribution.asks\n        )\n        bids = collections.deque(\n            OrderData(\n                trading_enums.TradeOrderSide.BUY,\n                book_order.amount,\n                book_order.price,\n                self.symbol,\n\n            )\n            for book_order in distribution.bids\n        )\n        self.logger.info(\n            f\"{self.symbol} {self.exchange_manager.exchange_name} target market marking orders: \"\n            f\"{len(bids)} bids & {len(asks)} asks: {bids=} {asks=}\"\n        )\n        # alternate by and sell orders to create book from the inside out\n        while asks and bids:\n            orders.append(asks.pop())\n            orders.append(bids.pop())\n        # add remaining orders if any\n        if asks:\n            orders += list(asks)\n        if bids:\n            orders += list(bids)\n        return orders\n\n    def _get_daily_volume(self, reference_price: decimal.Decimal) -> (decimal.Decimal, decimal.Decimal):\n        symbol_data = self.exchange_manager.exchange_symbols_data.get_exchange_symbol_data(\n            self.symbol, allow_creation=False\n        )\n        try:\n            return trading_api.get_daily_base_and_quote_volume(symbol_data, reference_price)\n        except ValueError as err:\n            raise ValueError(\n                f\"Missing volume for {self.symbol} on {self.exchange_manager.exchange_name}: \"\n                f\"{err}. {reference_price=}\"\n            ) from err\n\n    def _get_available_funds(self) -> (decimal.Decimal, decimal.Decimal):\n        base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        return (\n            trading_api.get_portfolio_currency(self.exchange_manager, base).available,\n            trading_api.get_portfolio_currency(self.exchange_manager, quote).available\n        )\n\n    def _get_all_theoretically_available_funds(self, open_orders: list) -> (decimal.Decimal, decimal.Decimal):\n        technically_available_base, technically_available_quote = self._get_available_funds()\n        for order in open_orders:\n            # order.filled_quantity is not handled in simulator\n            filled_quantity = trading_constants.ZERO if self.exchange_manager.trader.simulate else order.filled_quantity\n            if order.side == trading_enums.TradeOrderSide.BUY:\n                initial_cost = order.origin_quantity * order.origin_price\n                filled_cost = filled_quantity * order.filled_price\n                technically_available_quote += initial_cost - filled_cost\n            elif order.side == trading_enums.TradeOrderSide.SELL:\n                technically_available_base += order.origin_quantity - filled_quantity\n        return technically_available_base, technically_available_quote\n\n    def get_market_making_orders(self) -> list[trading_personal_data.Order]:\n        return [\n            order\n            for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(\n                symbol=self.symbol\n            )\n            # exclude market and stop orders\n            if isinstance(order, (trading_personal_data.BuyLimitOrder, trading_personal_data.SellLimitOrder))\n        ]\n\n    def _is_missing_open_orders(\n        self, sided_orders: list[trading_personal_data.Order], side: trading_enums.TradeOrderSide\n    ) -> bool:\n        if not sided_orders:\n            # no orders on this side: orders are missing\n            return True\n        if (last_target_orders_count := (\n            self.last_target_buy_orders_count\n            if side == trading_enums.TradeOrderSide.BUY\n            else self.last_target_sell_orders_count\n        )) and (len(sided_orders) < last_target_orders_count) and not self._is_previous_plan_still_processing():\n            self.logger.info(\n                f\"Missing {last_target_orders_count - len(sided_orders)} {self.symbol} {side.value} \"\n                f\"orders [{self.exchange_manager.exchange_name}], last target count: {last_target_orders_count}\"\n            )\n            # at least one order is missing compared to the last check\n            return True\n        return False\n\n    async def on_new_reference_price(self, reference_price: decimal.Decimal) -> bool:\n        trigger = False\n        open_orders = self.get_market_making_orders()\n        buy_orders = [\n            order\n            for order in open_orders\n            if order.side == trading_enums.TradeOrderSide.BUY\n        ]\n        if self._is_missing_open_orders(buy_orders, trading_enums.TradeOrderSide.BUY):\n            trigger = True\n        else:\n            max_buy_price = max(order.origin_price for order in buy_orders)\n            if max_buy_price > reference_price:\n                trigger = True\n        sell_orders = [\n            order\n            for order in open_orders\n            if order.side == trading_enums.TradeOrderSide.SELL\n        ]\n        if self._is_missing_open_orders(sell_orders, trading_enums.TradeOrderSide.SELL):\n            trigger = True\n        else:\n            min_sell_price = min(order.origin_price for order in sell_orders)\n            if min_sell_price < reference_price:\n                trigger = True\n        return trigger\n\n    async def _on_reference_price_update(self):\n        trigger = False\n        if reference_price := await self._get_reference_price():\n            trigger = await self.on_new_reference_price(reference_price)\n        if trigger:\n            await self._ensure_market_making_orders(f\"reference price update: {float(reference_price)}\")\n\n    async def order_filled_callback(self, order: dict):\n        self.logger.info(\n            f\"Triggering {self.symbol} [{self.exchange_manager.exchange_name}] order update an order got filled: \"\n            f\"{order}\"\n        )\n        await self._ensure_market_making_orders(\n            f\"filled {order[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]} order\"\n        )\n\n    async def _mark_price_callback(\n        self, exchange: str, exchange_id: str, cryptocurrency: str, symbol: str, mark_price\n    ):\n        \"\"\"\n        Called on a price update from an exchange that is different from the current one\n        :param exchange: name of the exchange\n        :param exchange_id: id of the exchange\n        :param cryptocurrency: related cryptocurrency\n        :param symbol: related symbol\n        :param mark_price: updated mark price\n        :return: None\n        \"\"\"\n        await self._on_reference_price_update()\n\n    async def _subscribe_to_exchange_mark_price(self, exchange_id: str, exchange_manager):\n        specs = trading_exchanges.ChannelSpecs(\n            trading_constants.MARK_PRICE_CHANNEL,\n            self.trading_mode.symbol,\n            None\n        )\n        if not self.already_subscribed_to_channel(exchange_id, specs):\n            await exchanges_channel.get_chan(trading_constants.MARK_PRICE_CHANNEL, exchange_id).new_consumer(\n                callback=self._mark_price_callback,\n                symbol=self.trading_mode.symbol\n            )\n            if exchange_id not in self.subscribed_channel_specs_by_exchange_id:\n                self.subscribed_channel_specs_by_exchange_id[exchange_id] = set()\n            self.subscribed_channel_specs_by_exchange_id[exchange_id].add(specs)\n            self.logger.info(\n                f\"{self.trading_mode.get_name()} for {self.trading_mode.symbol} on {self.exchange_name}:  \"\n                f\"{exchange_manager.exchange_name} price data feed.\"\n            )\n\n    def already_subscribed_to_channel(self, exchange_id: str, specs: trading_exchanges.ChannelSpecs) -> bool:\n        return (\n            exchange_id in self.subscribed_channel_specs_by_exchange_id\n            and specs in self.subscribed_channel_specs_by_exchange_id[exchange_id]\n        )\n\n    async def _get_reference_price(self) -> decimal.Decimal:\n        local_exchange_name = self.exchange_manager.exchange_name\n        price = trading_constants.ZERO\n        for exchange_id in trading_api.get_all_exchange_ids_with_same_matrix_id(\n            local_exchange_name, self.exchange_manager.id\n        ):\n            exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)\n            if exchange_manager.trading_modes and exchange_manager is not self.exchange_manager:\n                await self.sent_once_critical_notification(\n                    \"Configuration issue\",\n                    f\"Multiple simultaneous trading exchanges is not supported on {self.trading_mode.get_name()}\"\n                )\n            other_exchange_key = self.trading_mode.LOCAL_EXCHANGE_PRICE if (\n                self.trading_mode.LOCAL_EXCHANGE_PRICE == self.reference_price.exchange\n                and local_exchange_name == exchange_manager.exchange_name\n            ) else exchange_manager.exchange_name\n            if other_exchange_key != self.reference_price.exchange:\n                continue\n            if exchange_id not in self.subscribed_exchange_ids:\n                await self._subscribe_to_exchange_mark_price(exchange_id, exchange_manager)\n            try:\n                price, updated = trading_personal_data.get_potentially_outdated_price(\n                    exchange_manager, self.reference_price.pair\n                )\n                if not updated:\n                    self.logger.warning(\n                        f\"{exchange_manager.exchange_name} mark price: {price} is outdated for {self.symbol}. \"\n                        f\"Using it anyway\"\n                    )\n            except KeyError:\n                method = self.logger.info if self.is_first_execution else (\n                    self.logger.error if (\n                        self.exchange_manager.exchange.get_exchange_current_time() - self._started_at\n                        > self.REFERENCE_PRICE_INIT_DELAY\n                    )\n                    else self.logger.warning()\n                )\n                method(\n                    f\"No {exchange_manager.exchange_name} exchange symbol data for {self.symbol}, \"\n                    f\"it's probably initializing\"\n                )\n        return price\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"MarketMakingTradingMode\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/order_book_distribution.py",
    "content": "# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport copy\nimport dataclasses\nimport decimal\nimport typing\n\nimport octobot_commons.logging as commons_logging\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.personal_data as trading_personal_data\n\n\nDEFAULT_TOLERATED_BELLOW_DEPTH_RATIO = decimal.Decimal(\"0.80\")\nDEFAULT_TOLERATED_ABOVE_DEPTH_RATIO = decimal.Decimal(\"1.50\")\nALLOWED_MIN_SPREAD_RATIO = decimal.Decimal(\"0.1\")\nALLOWED_MAX_SPREAD_RATIO = decimal.Decimal(\"0.1\")\nTARGET_CUMULATED_VOLUME_PERCENT: decimal.Decimal = decimal.Decimal(3)\nDAILY_TRADING_VOLUME_PERCENT: decimal.Decimal = decimal.Decimal(2)\nMAX_HANDLED_BIDS_ORDERS = 5\nMAX_HANDLED_ASKS_ORDERS = 5\n\nINCREASING = \"increasing_towards_current_price\"\nDECREASING = \"decreasing_towards_current_price\"\n\n# allow up to 10 decimals to avoid floating point precision issues due to percent ratios\n_MAX_PRECISION = decimal.Decimal(\"1.0000000000\")\n\n@dataclasses.dataclass\nclass InferredOrderData:\n    ideal_price: decimal.Decimal\n    ideal_amount_percent: decimal.Decimal\n    current_price: typing.Optional[decimal.Decimal]\n    current_origin_amount: typing.Optional[decimal.Decimal]\n    final_amount: typing.Optional[decimal.Decimal]\n    final_price: typing.Optional[decimal.Decimal]\n\n\n@dataclasses.dataclass\nclass BookOrderData:\n    price: decimal.Decimal\n    amount: decimal.Decimal\n    side: trading_enums.TradeOrderSide\n\n    def get_base_amount(self) -> decimal.Decimal:\n        return self.amount * self.price if self.side == trading_enums.TradeOrderSide.BUY else self.amount\n\n\nclass FullBookRebalanceRequired(Exception):\n    pass\n\n\nclass MissingOrderException(Exception):\n    pass\n\n\nclass MissingAllBids(MissingOrderException):\n    pass\n\n\nclass MissingAllAsks(MissingOrderException):\n    pass\n\n\nclass MissingAllOrders(MissingOrderException):\n    pass\n\n\nclass OrderBookDistribution:\n    def __init__(\n        self,\n        bids_count: int,\n        asks_count: int,\n        min_spread: decimal.Decimal,\n        max_spread: decimal.Decimal,\n    ):\n        self.min_spread: decimal.Decimal = min_spread\n        self.max_spread: decimal.Decimal = max_spread\n        self.bids_count: int = bids_count\n        self.asks_count: int = asks_count\n\n        self.bids: list[BookOrderData] = []\n        self.asks: list[BookOrderData] = []\n\n    def get_ideal_total_volume(\n        self, side: trading_enums.TradeOrderSide, reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal, daily_quote_volume: decimal.Decimal,\n    ) -> decimal.Decimal:\n        orders_count, start_price, end_price, reference_volume, available_funds = self._get_sided_orders_details(\n            side, reference_price, daily_base_volume, daily_quote_volume, None, None, []\n        )\n\n        # order prices are sorted from the inside out of the order book (closest to the price first)\n        order_prices = self._get_order_prices(start_price, end_price, orders_count)\n\n        return self._get_total_volume_to_use(\n            side, reference_price, reference_volume, order_prices, available_funds, False\n        )\n\n    def compute_distribution(\n        self,\n        reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal, daily_quote_volume: decimal.Decimal,\n        symbol_market: dict,\n        available_base: typing.Optional[decimal.Decimal] = None,\n        available_quote: typing.Optional[decimal.Decimal] = None,\n    ):\n        self.bids = self._get_target_orders(\n            trading_enums.TradeOrderSide.BUY, reference_price,\n            daily_base_volume, daily_quote_volume, available_base, available_quote,\n            symbol_market\n        )\n        self.asks = self._get_target_orders(\n            trading_enums.TradeOrderSide.SELL, reference_price,\n            daily_base_volume, daily_quote_volume, available_base, available_quote,\n            symbol_market\n        )\n        return self\n\n    def get_shape_distance_from(\n        self,\n        orders: list[BookOrderData],\n        available_base: decimal.Decimal,\n        available_quote: decimal.Decimal,\n        reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal,\n        daily_quote_volume: decimal.Decimal,\n        trigger_source: str,\n    ) -> float:\n        \"\"\"\n        Returns a float averaging the distance of each given order relatively to the ideal\n        configured order volumes shape\n        \"\"\"\n        bids_difference = self._get_sided_orders_distance_from_ideal(\n            orders, available_quote, reference_price, daily_quote_volume,\n            trading_enums.TradeOrderSide.BUY, trigger_source\n        )\n        asks_difference = self._get_sided_orders_distance_from_ideal(\n            orders, available_base, reference_price, daily_base_volume,\n            trading_enums.TradeOrderSide.SELL, trigger_source\n        )\n        return float(bids_difference + asks_difference) / 2\n\n    def is_spread_according_to_config(self, orders: list[BookOrderData], open_orders: list[trading_personal_data.Order]):\n        open_buy_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        open_sell_orders = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        if not (open_buy_orders and open_sell_orders):\n            # missing all buy or sell orders (or both)\n            if not (open_buy_orders or open_sell_orders):\n                raise MissingAllOrders()\n            if not open_buy_orders:\n                raise MissingAllBids()\n            if not open_sell_orders:\n                raise MissingAllAsks()\n        if not (len(open_buy_orders) == self.bids_count and len(open_sell_orders) == self.asks_count):\n            # missing a few orders, spread can't be checked, consider valid\n            return True\n        buy_orders = get_sorted_sided_orders([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY], True)\n        sell_orders = get_sorted_sided_orders([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL], True)\n        min_spread = (sell_orders[0].price - buy_orders[0].price)/(\n            (sell_orders[0].price + buy_orders[0].price) / decimal.Decimal(\"2\")\n        )\n        max_spread = (sell_orders[-1].price - buy_orders[-1].price)/(\n            (sell_orders[-1].price + buy_orders[-1].price) / decimal.Decimal(\"2\")\n        )\n        compliant_spread = (\n            (\n                self.min_spread * (trading_constants.ONE - ALLOWED_MIN_SPREAD_RATIO)\n                < min_spread\n                < self.min_spread * (trading_constants.ONE + ALLOWED_MIN_SPREAD_RATIO)\n            )\n            and (\n                self.max_spread * (trading_constants.ONE - ALLOWED_MAX_SPREAD_RATIO)\n                < max_spread\n                < self.max_spread * (trading_constants.ONE + ALLOWED_MAX_SPREAD_RATIO)\n            )\n        )\n        if not compliant_spread:\n            self.get_logger().warning(\n                f\"Spread is beyond configuration: {min_spread=} {self.min_spread=} {max_spread=} {self.max_spread=}\"\n            )\n        return compliant_spread\n\n    def infer_full_order_data_after_swaps(\n        self,\n        existing_orders: list[BookOrderData],\n        outdated_orders: list[trading_personal_data.Order],\n        available_base: decimal.Decimal,\n        available_quote: decimal.Decimal,\n        reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal,\n        daily_quote_volume: decimal.Decimal,\n    ):\n        \"\"\"\n        return the target updated list of BookOrderData using existing_orders as the current state of the order book\n        and the current configuration\n        \"\"\"\n        buy_orders = [o for o in existing_orders if o.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [o for o in existing_orders if o.side == trading_enums.TradeOrderSide.SELL]\n        updated_existing_orders = copy.copy(existing_orders)\n        if len(buy_orders) < len(sell_orders):\n            # missing buy orders: create missing buy order based on current sell orders\n            adapted_buy_orders = self._infer_sided_order_data_after_swaps(\n                updated_existing_orders, outdated_orders, available_quote, reference_price,\n                daily_quote_volume, trading_enums.TradeOrderSide.BUY\n            )\n            updated_existing_orders = [o for o in existing_orders if o.side != trading_enums.TradeOrderSide.BUY]\n            # compute sell orders based on adapted buy orders\n            updated_existing_orders += adapted_buy_orders\n            adapted_sell_orders = self._infer_sided_order_data_after_swaps(\n                updated_existing_orders, outdated_orders, available_base, reference_price,\n                daily_base_volume, trading_enums.TradeOrderSide.SELL\n            )\n        else:\n            # missing sell orders (or both sides): create missing sell order based on current buy orders\n            adapted_sell_orders = self._infer_sided_order_data_after_swaps(\n                updated_existing_orders, outdated_orders, available_base, reference_price,\n                daily_base_volume, trading_enums.TradeOrderSide.SELL\n            )\n            updated_existing_orders = [o for o in existing_orders if o.side != trading_enums.TradeOrderSide.SELL]\n            # compute sell orders based on adapted buy orders\n            updated_existing_orders += adapted_sell_orders\n            adapted_buy_orders = self._infer_sided_order_data_after_swaps(\n                updated_existing_orders, outdated_orders, available_quote, reference_price,\n                daily_quote_volume, trading_enums.TradeOrderSide.BUY\n            )\n        return adapted_buy_orders + adapted_sell_orders\n\n    def _get_sided_orders_distance_from_ideal(\n        self,\n        orders: list[BookOrderData],\n        available_funds: decimal.Decimal,\n        reference_price: decimal.Decimal,\n        daily_volume: decimal.Decimal,\n        side: trading_enums.TradeOrderSide,\n        trigger_source: str,\n    ):\n        # shape distance is computed using the average % difference from the ideal shape of the book\n        closer_to_further_real_orders = get_sorted_sided_orders(\n            [o for o in orders if o.side == side], True\n        )\n        ideal_orders_count = self.bids_count if side == trading_enums.TradeOrderSide.BUY else self.asks_count\n        if not closer_to_further_real_orders:\n            if ideal_orders_count > 0:\n                self.get_logger().info(\n                    f\"0 {side.name} open orders, required: {ideal_orders_count} refresh required \"\n                    f\"[trigger source: {trigger_source}]\"\n                )\n                return 1\n            return 0\n        if not self._are_total_order_volumes_compatible_with_config(\n            closer_to_further_real_orders, available_funds, reference_price,daily_volume, side, trigger_source\n        ):\n            return 1\n        min_amount, max_amount = (\n            min(closer_to_further_real_orders[0].amount, closer_to_further_real_orders[-1].amount),\n            max(closer_to_further_real_orders[0].amount, closer_to_further_real_orders[-1].amount)\n        )\n        ideal_prices = self._get_order_prices(decimal.Decimal(0), trading_constants.ONE_HUNDRED, ideal_orders_count)\n        raw_ideal_amounts = self._get_order_volumes(side, trading_constants.ONE_HUNDRED, ideal_prices)\n        min_ideal_amount, max_ideal_amount = min(raw_ideal_amounts), max(raw_ideal_amounts)\n        if max_amount == trading_constants.ZERO or max_ideal_amount == trading_constants.ZERO:\n            # impossible to compute distance\n            self.get_logger().info(\n                f\"Incompatible total amounts on {side.name} side: {max_amount=}, {max_ideal_amount=}, refresh required \"\n                f\"[trigger source: {trigger_source}]\"\n            )\n            return 1\n        # align amounts between 0 and 100 to be able to compare\n        real_amounts = [\n            decimal.Decimal(str((o.amount - min_amount) * trading_constants.ONE_HUNDRED / max_amount))\n            for o in closer_to_further_real_orders\n        ]\n        ideal_amounts = [\n            (a - min_ideal_amount) * trading_constants.ONE_HUNDRED / max_ideal_amount\n            for a in raw_ideal_amounts\n        ]\n        distances = []\n        for i, ideal_amount in enumerate(ideal_amounts):\n            try:\n                distances.append(abs(ideal_amount - real_amounts[i]) / trading_constants.ONE_HUNDRED)\n            except IndexError:\n                # missing price\n                distances.append(trading_constants.ZERO)\n        if len(real_amounts) > len(ideal_amounts):\n            # real orders that should not be open\n            distances += [decimal.Decimal(1)] * (len(real_amounts) - len(ideal_amounts))\n        return (sum(distances) / len(distances)) if distances else 0\n\n    def _should_use_artificial_funds(\n        self, ideal_total_volume: decimal.Decimal, total_volume: decimal.Decimal,\n        side: trading_enums.TradeOrderSide, tolerated_bellow_depth_ratio=DEFAULT_TOLERATED_BELLOW_DEPTH_RATIO\n    ) -> bool:\n        return ideal_total_volume * tolerated_bellow_depth_ratio > total_volume\n\n    def _are_total_order_volumes_compatible_with_config(\n        self,\n        closer_to_further_real_orders: list[BookOrderData],\n        available_funds: decimal.Decimal,\n        reference_price: decimal.Decimal,\n        daily_volume: decimal.Decimal,\n        side: trading_enums.TradeOrderSide,\n        trigger_source: str,\n        tolerated_bellow_depth_ratio = DEFAULT_TOLERATED_BELLOW_DEPTH_RATIO,\n        tolerated_above_depth_ratio = DEFAULT_TOLERATED_ABOVE_DEPTH_RATIO,\n    ) -> bool:\n        order_prices = [o.price for o in closer_to_further_real_orders]\n        ideal_total_volume = self._get_ideal_total_volume_to_use(\n            side, reference_price, daily_volume, order_prices, False\n        )\n        total_volume = self._get_total_volume_to_use(\n            side, reference_price, daily_volume, order_prices, available_funds, False\n        )\n        if self._should_use_artificial_funds(ideal_total_volume, total_volume, side):\n            # case 1. not enough funds and compliant config to use ideal volume: check all orders total amount\n            #   against available funds (taken into account in total_volume)\n            # case 2. enough funds and non-compliant config to use ideal volume: check all orders total amount\n            #   against target config (taken into account in total_volume)\n            # case 3. both cases 1. and 2. => same outcome\n            theoretical_used_funds = total_volume\n            current_used_funds = sum(\n                order.get_base_amount()\n                for order in closer_to_further_real_orders\n            )\n            required_source = \"available funds or config\"\n        else:\n            # case 4: enough funds and compliant config to use ideal volume: check orders market_depth_size\n            #   against total volume before market depth threshold\n            theoretical_used_funds = self._get_total_volume_to_use(\n                side, reference_price, daily_volume, order_prices, available_funds, True\n            )\n            current_used_funds = sum(\n                amount\n                for amount in self._get_market_depth_order_amounts(closer_to_further_real_orders, reference_price)\n            )\n            required_source = \"ideal funds according to config and trading volume\"\n        if current_used_funds < theoretical_used_funds * tolerated_bellow_depth_ratio:\n            self.get_logger().warning(\n                f\"{side.name} order book depth is not reached, refresh required. \"\n                f\"Volume in orders: {current_used_funds}, required: {theoretical_used_funds} (from {required_source}) \"\n                f\"[trigger source: {trigger_source}]\"\n            )\n            return False\n        if current_used_funds > theoretical_used_funds * tolerated_above_depth_ratio:\n            self.get_logger().warning(\n                f\"{side.name} order book depth is exceeded by more than \"\n                f\"{tolerated_above_depth_ratio * trading_constants.ONE_HUNDRED - trading_constants.ONE_HUNDRED}%, \"\n                f\"refresh required. Volume in orders: {current_used_funds}, required: {theoretical_used_funds} \"\n                f\"(from {required_source}) \"\n                f\"[trigger source: {trigger_source}]\"\n            )\n            return False\n        return True\n\n    def _get_target_orders(\n        self, side: trading_enums.TradeOrderSide, reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal, daily_quote_volume: decimal.Decimal,\n        available_base: typing.Optional[decimal.Decimal], available_quote: typing.Optional[decimal.Decimal],\n        symbol_market: dict\n    ) -> list[BookOrderData]:\n        orders_count, start_price, end_price, reference_volume, available_funds = self._get_sided_orders_details(\n            side, reference_price, daily_base_volume, daily_quote_volume, available_base, available_quote, []\n        )\n\n        # order prices are sorted from the inside out of the order book (closest to the price first)\n        order_prices = self._get_order_prices(start_price, end_price, orders_count)\n\n        total_volume = self._get_total_volume_to_use(\n            side, reference_price, reference_volume, order_prices, available_funds, False\n        )\n        # order volumes are sorted from the inside out of the order book (closest to the price first)\n        order_volumes = self._get_order_volumes(side, total_volume, order_prices)\n        if side is trading_enums.TradeOrderSide.BUY:\n            # convert quote volume into base\n            order_volumes = [\n                (volume / order_price) if order_price else volume\n                for volume, order_price in zip(order_volumes, order_prices)\n            ]\n\n        if len(order_prices) != len(order_volumes):\n            raise ValueError(f\"order_prices and order_volumes should have the same size\")\n\n        return [\n            BookOrderData(\n                trading_personal_data.decimal_adapt_price(symbol_market, price),\n                trading_personal_data.decimal_adapt_quantity(symbol_market, volume),\n                side,\n            )\n            for price, volume in zip(order_prices, order_volumes)\n        ]\n\n    def can_create_at_least_one_order(self, sides: list[trading_enums.TradeOrderSide], symbol_market: dict) -> bool:\n        for side in sides:\n            orders = self.bids if side == trading_enums.TradeOrderSide.BUY else self.asks\n            if not self._is_at_least_one_order_valid(orders, symbol_market):\n                return False\n        return True\n\n    def _is_at_least_one_order_valid(self, orders: list[BookOrderData], symbol_market: dict) -> bool:\n        for order in orders:\n            if trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                order.amount,\n                order.price,\n                symbol_market\n            ):\n                return True\n        return False\n\n    def validate_config(self):\n        if self.asks_count > MAX_HANDLED_ASKS_ORDERS:\n            raise ValueError(\n                f\"A maximum of {MAX_HANDLED_ASKS_ORDERS} asks is supported\"\n            )\n        if self.bids_count > MAX_HANDLED_BIDS_ORDERS:\n            raise ValueError(\n                f\"A maximum of {MAX_HANDLED_BIDS_ORDERS} bids is supported\"\n            )\n        if self.max_spread <= self.min_spread:\n            raise ValueError(\n                f\"Maximum spread ({float(self.max_spread)}) must be larger than \"\n                f\"minimum spread ({float(self.min_spread)}).\"\n            )\n        allowed_min_spread = decimal.Decimal(\"2\") * TARGET_CUMULATED_VOLUME_PERCENT / trading_constants.ONE_HUNDRED\n        if self.min_spread > allowed_min_spread:\n            raise ValueError(\n                f\"Minimum spread should be smaller than {allowed_min_spread}. \"\n                f\"Minimum spread: {float(self.min_spread)}\"\n            )\n\n    def _get_sided_orders_details(\n        self, side: trading_enums.TradeOrderSide, reference_price: decimal.Decimal,\n        daily_base_volume: decimal.Decimal, daily_quote_volume: decimal.Decimal,\n        available_base: typing.Optional[decimal.Decimal], available_quote: typing.Optional[decimal.Decimal],\n        other_side_orders: list[BookOrderData]\n    ):\n        self.validate_config()\n        # reverse when other side is BUY, therefore current side is sell\n        first_other_side_price = get_sorted_sided_orders(\n            other_side_orders, True\n        )[0] if other_side_orders else None\n        order_book_price_range = reference_price * (self.max_spread - self.min_spread) / decimal.Decimal(\"2\")\n        flat_min_spread = reference_price * self.min_spread\n        if side is trading_enums.TradeOrderSide.BUY:\n            orders_count = self.bids_count\n            if first_other_side_price is None or first_other_side_price.price - flat_min_spread > reference_price:\n                start_price = reference_price - (flat_min_spread / 2)\n            else:\n                start_price = first_other_side_price.price - flat_min_spread\n            end_price = start_price - order_book_price_range\n            reference_volume = daily_quote_volume\n            available_funds = available_quote\n        else:\n            orders_count = self.asks_count\n            if first_other_side_price is None or first_other_side_price.price + flat_min_spread < reference_price:\n                start_price = reference_price + (flat_min_spread / 2)\n            else:\n                start_price = first_other_side_price.price + flat_min_spread\n            end_price = start_price + order_book_price_range\n            reference_volume = daily_base_volume\n            available_funds = available_base\n        return orders_count, start_price, end_price, reference_volume, available_funds\n\n    def _get_order_prices(\n        self, start_price: decimal.Decimal, end_price: decimal.Decimal, orders_count: int\n    ) -> list[decimal.Decimal]:\n        if orders_count < 2:\n            raise ValueError(\"Orders count must be greater than 2\")\n        increment = (end_price - start_price) / (orders_count - 1)\n        return [\n            start_price + (increment * i)\n            for i in range(orders_count)\n        ]\n\n    def _infer_sided_order_data_after_swaps(\n        self,\n        existing_orders: list[BookOrderData],\n        outdated_orders: list[trading_personal_data.Order],\n        available_funds: decimal.Decimal,\n        reference_price: decimal.Decimal,\n        reference_volume: decimal.Decimal,\n        side: trading_enums.TradeOrderSide\n    ) -> list[BookOrderData]:\n        if not existing_orders and not outdated_orders:\n            # nothing to adapt: return ideal orders\n            return self.bids if side == trading_enums.TradeOrderSide.BUY else self.asks\n        closer_to_further_orders = get_sorted_sided_orders(\n            [o for o in existing_orders if o.side == side], True\n        )\n        other_side_orders = [o for o in existing_orders if o.side != side]\n        orders_count, ideal_start_price, ideal_end_price, _, _ = self._get_sided_orders_details(\n            side, reference_price,\n            trading_constants.ZERO, trading_constants.ZERO,\n            trading_constants.ZERO, trading_constants.ZERO,\n            other_side_orders,\n        )\n        ideal_prices = self._get_order_prices(ideal_start_price, ideal_end_price, orders_count)\n        ideal_amount_percents = self._get_order_volumes(side, trading_constants.ONE_HUNDRED, ideal_prices)\n        adapted_orders_data = []\n        existing_order_index = 0\n        moving_window_price_ratio = decimal.Decimal(\"1.5\")\n        for i in range(0, len(ideal_prices)):\n            ideal_price = ideal_prices[i]\n            inferred_order_data = InferredOrderData(\n                ideal_price, ideal_amount_percents[i], None, None, None, ideal_price\n            )\n            previous_ideal_price = ideal_prices[i - 1] if i > 0 else reference_price\n            next_ideal_price = ideal_prices[i + 1] if i < len(ideal_prices) - 1 else None\n            window_min = ideal_price - (\n                abs(ideal_price - previous_ideal_price) / (\n                    moving_window_price_ratio if i > 0 else decimal.Decimal(1)\n                )\n            )\n            window_max = ideal_price + (\n                (abs(next_ideal_price - ideal_price) / moving_window_price_ratio)\n                if next_ideal_price is not None\n                # fallback to previous price increment\n                else abs(ideal_price - previous_ideal_price)\n            )\n            # for each ideal price, check if an equivalent exists in current prices\n            candidate_existing_order_index = existing_order_index\n            found_order = False\n            while not found_order and len(closer_to_further_orders) > candidate_existing_order_index:\n                current_order = closer_to_further_orders[candidate_existing_order_index]\n                if window_min <= current_order.price <= window_max:\n                    # price and amount are found: keep them\n                    inferred_order_data.current_price = current_order.price\n                    inferred_order_data.final_price = current_order.price\n                    inferred_order_data.current_origin_amount = current_order.amount\n                    inferred_order_data.final_amount = current_order.amount\n                    found_order = True\n                candidate_existing_order_index += 1\n                if found_order:\n                    # skip existing order from checked orders\n                    existing_order_index = candidate_existing_order_index\n                else:\n                    # price is missing: it will have to be added\n                    pass\n            adapted_orders_data.append(inferred_order_data)\n\n        self._adapt_inferred_order_amounts(\n            adapted_orders_data, existing_orders, outdated_orders,\n            available_funds, reference_price, reference_volume, side\n        )\n\n        return [\n            BookOrderData(order.final_price, order.final_amount, side)\n            for order in adapted_orders_data\n        ]\n\n    def _adapt_inferred_order_amounts(\n        self,\n        adapted_orders_data: list[InferredOrderData],\n        existing_orders: list[BookOrderData],\n        outdated_orders: list[trading_personal_data.Order],\n        available_funds: decimal.Decimal,\n        reference_price: decimal.Decimal,\n        reference_volume: decimal.Decimal,\n        side: trading_enums.TradeOrderSide\n    ):\n        if not any(d.final_amount is None for d in adapted_orders_data):\n            # nothing to adapt\n            return\n        # order.filled_quantity is not handled in simulator\n        available_funds_after_outdated_orders_in_quote_or_base = available_funds + sum(\n            (order.origin_quantity - (\n                trading_constants.ZERO if order.trader.simulate else order.filled_quantity\n            )) * order.origin_price\n            if side == trading_enums.TradeOrderSide.BUY else (order.origin_quantity - (\n                trading_constants.ZERO if order.trader.simulate else order.filled_quantity\n            ))\n            for order in outdated_orders\n            if order.side == side\n        )\n        # index missing final amounts\n        reused_order_prices = [\n            order.final_price\n            for order in adapted_orders_data\n            if order.current_origin_amount is not None\n        ]\n        cancelled_orders = [\n            order\n            for order in existing_orders\n            if order.side == side and order.price not in reused_order_prices\n        ]\n        available_funds_after_cancelled_orders_in_quote_or_base = sum([\n            (order.price * order.amount) if order.side == trading_enums.TradeOrderSide.BUY else order.amount\n            for order in cancelled_orders\n        ])\n        total_available_amount_in_quote_or_base = (\n            available_funds_after_outdated_orders_in_quote_or_base\n            + available_funds_after_cancelled_orders_in_quote_or_base\n        )\n        base_total_available_amount = (\n            total_available_amount_in_quote_or_base / reference_price\n            if side == trading_enums.TradeOrderSide.BUY else total_available_amount_in_quote_or_base\n        )\n\n        # infer missing order amounts using found order and ideal percents\n        if base_inferred_amounts := [\n            o.current_origin_amount * trading_constants.ONE_HUNDRED / o.ideal_amount_percent\n            for o in adapted_orders_data\n            if o.current_origin_amount is not None\n        ]:\n            # use existing orders when possible\n            base_inferred_amount_total_used_amount = sum(base_inferred_amounts) / len(base_inferred_amounts)\n        else:\n            # otherwise use config\n            order_prices = [\n                order.final_price\n                for order in adapted_orders_data\n            ]\n            inferred_amount_total_used_amount_in_quote_or_base = self._get_total_volume_to_use(\n                side, reference_price, reference_volume, order_prices, total_available_amount_in_quote_or_base,\n                False\n            )\n            base_inferred_amount_total_used_amount = (\n                inferred_amount_total_used_amount_in_quote_or_base / reference_price\n                if side == trading_enums.TradeOrderSide.BUY else inferred_amount_total_used_amount_in_quote_or_base\n            )\n\n        # get total amount in current orders\n        amount_in_orders = sum(\n            o.current_origin_amount\n            for o in adapted_orders_data\n            if o.current_origin_amount is not None\n        )\n        # compute missing amount\n        base_missing_amount = base_inferred_amount_total_used_amount - amount_in_orders\n        if base_missing_amount < trading_constants.ZERO:\n            # Means that required amount is lower than current open amount even though orders are missing. This\n            # usually means that trading volume decreased and therefore less quantity is now required.\n            # In this case, a full order book refresh is required\n            raise FullBookRebalanceRequired(\n                f\"Too much funds in order book: missing amount in orders is < 0: {base_missing_amount}: \"\n                f\"{base_inferred_amount_total_used_amount=} \"\n                f\"{amount_in_orders=} {adapted_orders_data=}\"\n            )\n        # if enough funds: use new %, otherwise adapt max to be available amount \"splitable\" between orders to create\n        base_usable_total_amount = base_missing_amount\n        if base_total_available_amount < base_missing_amount:\n            # default to available funds if base_missing_amount is not available\n            base_usable_total_amount = base_total_available_amount\n        splittable_base_missing_amount = base_usable_total_amount / sum(\n            inferred_data.ideal_amount_percent / trading_constants.ONE_HUNDRED\n            for inferred_data in adapted_orders_data\n            if inferred_data.current_origin_amount is None\n        )\n\n        for inferred_data in adapted_orders_data:\n            if inferred_data.current_origin_amount is None:\n                inferred_data.final_amount = (\n                    inferred_data.ideal_amount_percent / trading_constants.ONE_HUNDRED * splittable_base_missing_amount\n                )\n\n    def _get_order_volumes(\n        self, side: trading_enums.TradeOrderSide, total_volume: decimal.Decimal, order_prices: list[decimal.Decimal],\n        multiplier=decimal.Decimal(1), direction=DECREASING\n    ) -> list[decimal.Decimal]:\n        orders_count = len(order_prices)\n        if orders_count < 2:\n            raise ValueError(\"Orders count must be greater than 2\")\n        decimal_orders_count = decimal.Decimal(str(orders_count))\n        if direction in (INCREASING, DECREASING):\n            average_order_size = total_volume / decimal_orders_count\n            max_size_delta = average_order_size * (multiplier - 1)\n            increment = max_size_delta / decimal_orders_count\n\n            # base_vol + base_vol + increment + base_vol + 2 x increment + .... = total_volume\n            # order_count: 1 => 0 = 0 increment\n            # order_count: 2 => 0 + 1 = 1 increments\n            # order_count: 3 => 0 + 1 + 2 = 3 increments\n            # order_count: 4 => 0 + 1 + 2 + 3 = 6 increments\n            # order_count: 5 => 0 + 1 + 2 + 3 + 4 = 10 increments\n            total_increments = sum(i for i in range(orders_count))\n            base_vol = (total_volume - (total_increments * increment)) / decimal_orders_count\n\n            iterator = range(orders_count) if DECREASING else range(orders_count - 1, 0, -1)\n            # DECREASING : order are smaller when closer to the reference price\n            # INCREASING : order are larger when closer to the reference price\n            order_volumes = [\n                base_vol + (increment * decimal.Decimal(str(i)))\n                for i in iterator\n            ]\n        else:\n            raise NotImplementedError(f\"{direction} not implemented\")\n        return order_volumes\n\n    def _get_total_volume_to_use(\n        self, side: trading_enums.TradeOrderSide, reference_price: decimal.Decimal,\n        reference_volume: decimal.Decimal, order_prices: list[decimal.Decimal],\n        available_funds_base_or_quote: typing.Optional[decimal.Decimal],\n        until_depth_threshold_only: bool,\n    ) -> decimal.Decimal:\n        ideal_total_volume = self._get_ideal_total_volume_to_use(\n            side, reference_price, reference_volume, order_prices, until_depth_threshold_only\n        )\n        if available_funds_base_or_quote is not None and ideal_total_volume > available_funds_base_or_quote:\n            return available_funds_base_or_quote\n        return ideal_total_volume\n\n    def _get_market_depth_order_amounts(\n        self, orders: list[BookOrderData], reference_price: decimal.Decimal\n    ) -> list[decimal.Decimal]:\n        return [\n            order.get_base_amount()\n            for order in orders\n            if abs(trading_constants.ONE_HUNDRED - (\n                order.price * trading_constants.ONE_HUNDRED / reference_price\n            )) <= TARGET_CUMULATED_VOLUME_PERCENT\n        ]\n\n    def _get_ideal_total_volume_to_use(\n        self, side: trading_enums.TradeOrderSide, reference_price: decimal.Decimal,\n        reference_volume: decimal.Decimal, order_prices: list[decimal.Decimal],\n        until_depth_threshold_only: bool,\n        daily_trading_volume_percent=DAILY_TRADING_VOLUME_PERCENT\n    ) -> decimal.Decimal:\n        # ideal volume contains daily_trading_volume_percent of daily_volume\n        # within the first target_cumulated_volume_percent of the order book\n        target_before_threshold_volume = (\n            reference_volume * daily_trading_volume_percent / trading_constants.ONE_HUNDRED\n        )\n        if until_depth_threshold_only:\n            self.get_logger().info(f\"{target_before_threshold_volume=} {daily_trading_volume_percent=}\")\n            return target_before_threshold_volume\n        counted_orders = len(self._get_market_depth_order_amounts([\n            BookOrderData(price, trading_constants.ZERO, side)\n            for price in order_prices\n        ], reference_price))\n        # goal: the first (closes to reference price) counted_orders orders have a volume of target_volume\n\n        # use a percent-based volume profile to figure out the total required volume\n        reference_order_volumes = self._get_order_volumes(side, trading_constants.ONE_HUNDRED, order_prices)\n        # volume_before_threshold = % of traded volume that is contained before threshold\n        percent_volume_before_threshold = sum(\n            percent_volume\n            for percent_volume in reference_order_volumes[:counted_orders]\n        )\n        if percent_volume_before_threshold == trading_constants.ZERO:\n            if not reference_order_volumes:\n                raise ValueError(f\"Error: reference_order_volumes can't be empty. {order_prices=}\")\n            percent_volume_before_threshold = reference_order_volumes[0]\n        # ideal_total_volume = volume_before_threshold + rest of the volume\n        # ideal_total_volume = ideal_total_volume * percent_volume_before_threshold / 100 + ideal_total_volume * (1 - percent_volume_before_threshold / 100)\n        # Where: ideal_total_volume * percent_volume_before_threshold / 100 = target_before_threshold_volume\n        # Therefore: ideal_total_volume = target_before_threshold_volume + ideal_total_volume * (1 - percent_volume_before_threshold / 100)\n        # ideal_total_volume - ideal_total_volume * (100 - ideal_total_volume) = target_before_threshold_volume\n        # 1 - (1 - percent_volume_before_threshold / 100) = target_before_threshold_volume / ideal_total_volume\n        # ideal_total_volume = target_before_threshold_volume / (1 - (1 - percent_volume_before_threshold * 100))\n        ideal_total_volume = (\n            target_before_threshold_volume / percent_volume_before_threshold * trading_constants.ONE_HUNDRED\n        )\n        # keep up to 10 decimals to avoid floating point precision issues due to percent ratios\n        return _quantize_decimal(ideal_total_volume)\n\n    @classmethod\n    def get_logger(cls):\n        return commons_logging.get_logger(cls.__name__)\n\n\ndef _quantize_decimal(value: decimal.Decimal) -> decimal.Decimal:\n    return value.quantize(\n        _MAX_PRECISION, \n        rounding=decimal.ROUND_HALF_UP\n    )\n\n\ndef get_sorted_sided_orders(orders: list[BookOrderData], closer_to_further: bool) -> list[BookOrderData]:\n    if orders:\n        side = orders[0].side\n        return sorted(\n            orders,\n            key=lambda o: o.price,\n            reverse=side == (\n                trading_enums.TradeOrderSide.BUY if closer_to_further else trading_enums.TradeOrderSide.SELL\n            ),\n        )\n    return orders\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/reference_price.py",
    "content": "# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport dataclasses\nimport decimal\nimport enum\nimport typing\n\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.personal_data as trading_personal_data\n\n\n@dataclasses.dataclass\nclass PriceSource:\n    exchange: str\n    pair: str\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/resources/MarketMakingTradingMode.md",
    "content": "## MarketMakingTradingMode\n\nA market making strategy that will maintain the configured order book on the target exchange.\n\n### Behavior\nWhen started, the strategy will create orders according to its configuration. It might cancel open orders when\nthey are incompatible.\n\nAs soon as the maintained order book becomes outdated (from a changed reference price or filled/canceled orders), \nit will be adapted to always try to reflect the configuration.\n\nWhen a full order book replacement takes place, orders are canceled one by one to avoid leaving an empty book.\n\nThe strategy will use all available funds, up to a maximum of what is necessary to cover \n2% of the pair's daily trading volume on the target exchange within the first 3% of the order book depth.\n\nNote: The strategy does not create artificial volume by forcing market orders, it focuses on maintaining an optimized \norder book.\n\n### Configuration\n- Bids and asks counts define how many orders should be maintained within the book\n- Min spread is the distance (as a % of the current price) between the highest bid and lowest ask\n- Max spread is the distance (as a % of the current price) between the lowest bid and highest ask\n- Reference exchange is the exchange to get the current price of the traded pair from. It should be a very liquid exchange to avoid arbitrage opportunities.\n\nAn advanced version of this market making strategy is available on [OctoBot Market Making](https://market-making.octobot.cloud?utm_source=octobot_mm&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=trading_mode_docs).\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/tests/test_market_making_trading.py",
    "content": "# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport contextlib\nimport mock\nimport os\nimport pytest\n\n\nimport async_channel.util as channel_util\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.tests.test_config as test_config\nimport octobot_tentacles_manager.api as tentacles_manager_api\nimport octobot_backtesting.api as backtesting_api\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\nimport tentacles.Trading.Mode.market_making_trading_mode.market_making_trading as market_making_trading\n\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\nimport tests.test_utils.trading_modes as test_trading_modes\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n# binance symbol market extract\nSYMBOL_MARKET = {\n    'id': 'BTCUSDT', 'lowercaseId': 'btcusdt', 'symbol': 'BTC/USDT', 'base': 'BTC', 'quote': 'USDT',\n    'settle': None, 'baseId': 'BTC', 'quoteId': 'USDT', 'settleId': None, 'type': 'spot', 'spot': True,\n    'margin': True, 'swap': False, 'future': False, 'option': False, 'index': None, 'active': True,\n    'contract': False, 'linear': None, 'inverse': None, 'subType': None, 'taker': 0.001, 'maker': 0.001,\n    'contractSize': None, 'expiry': None, 'expiryDatetime': None, 'strike': None, 'optionType': None,\n    'precision': {'amount': 5, 'price': 2, 'cost': None, 'base': 1e-08, 'quote': 1e-08},\n    'limits': {\n        'leverage': {'min': None, 'max': None},\n        'amount': {'min': 1e-05, 'max': 9000.0},\n        'price': {'min': 0.01, 'max': 1000000.0},\n        'cost': {'min': 5.0, 'max': 9000000.0},\n        'market': {'min': 0.0, 'max': 107.1489592}\n    }, 'created': None,\n    'percentage': True, 'feeSide': 'get', 'tierBased': False\n}\n\ndef _get_mm_config():\n    return {\n      \"asks_count\": 5,\n      \"bids_count\": 5,\n      \"min_spread\": 5,\n      \"max_spread\": 20,\n      \"reference_exchange\": \"local\",\n    }\n\n\nasync def _init_trading_mode(config, exchange_manager, symbol):\n    mode = market_making_trading.MarketMakingTradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    mode.trading_config = _get_mm_config()\n    await mode.initialize(trading_config=mode.trading_config)\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n    test_trading_modes.set_ready_to_start(mode.producers[0])\n    return mode, mode.producers[0]\n\n\n@contextlib.asynccontextmanager\nasync def _get_tools(symbol, additional_portfolio={}):\n    tentacles_manager_api.reload_tentacle_info()\n    exchange_manager = None\n    try:\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 1000\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"BTC\"] = 10\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO].update(additional_portfolio)\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        # set BTC/USDT price at 1000 USDT\n        if symbol not in exchange_manager.client_symbols:\n            exchange_manager.client_symbols.append(symbol)\n        trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n        mode, producer = await _init_trading_mode(config, exchange_manager, symbol)\n\n        yield producer, mode.get_trading_mode_consumers()[0], exchange_manager\n    finally:\n        if exchange_manager:\n            await _stop(exchange_manager)\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n\n\nasync def test_handle_market_making_orders_from_no_orders():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol) as (producer, consumer, exchange_manager):\n        price = decimal.Decimal(1000)\n        origin_submit_trading_evaluation = producer.submit_trading_evaluation\n        with mock.patch.object(\n            producer, \"submit_trading_evaluation\", mock.AsyncMock(side_effect=origin_submit_trading_evaluation)\n        ) as submit_trading_evaluation_mock, mock.patch.object(\n            producer, \"_get_reference_price\", mock.AsyncMock(return_value=price)\n        ) as _get_reference_price_mock, mock.patch.object(\n            producer, \"_get_daily_volume\", mock.Mock(return_value=(decimal.Decimal(1), decimal.Decimal(1000)))\n        ) as _get_daily_volume_mock:\n            trigger_source = \"ref_price\"\n            # 1. full replace as no order exist\n            assert await producer._handle_market_making_orders(price, SYMBOL_MARKET, trigger_source, False) is True\n            _get_reference_price_mock.assert_called_once()\n            _get_daily_volume_mock.assert_called_once()\n            submit_trading_evaluation_mock.assert_called_once()\n            assert submit_trading_evaluation_mock.mock_calls[0].kwargs[\"symbol\"] == symbol\n            data = submit_trading_evaluation_mock.mock_calls[0].kwargs[\"data\"]\n            assert data[market_making_trading.MarketMakingTradingModeConsumer.CURRENT_PRICE_KEY] == price\n            assert data[market_making_trading.MarketMakingTradingModeConsumer.SYMBOL_MARKET_KEY] == SYMBOL_MARKET\n            order_plan: market_making_trading.OrdersUpdatePlan = data[market_making_trading.MarketMakingTradingModeConsumer.ORDER_ACTIONS_PLAN_KEY]\n            assert isinstance(order_plan, market_making_trading.OrdersUpdatePlan)\n            assert len(order_plan.order_actions) == 10\n            buy_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.BUY\n            ]\n            sell_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.SELL\n            ]\n            assert len(buy_actions) == len(sell_actions) == 5\n            assert order_plan.cancelled == False\n            assert order_plan.cancellable == False # full replace is not cancellable\n            assert not order_plan.processed.is_set()\n            assert order_plan.trigger_source == trigger_source\n\n            # wait for orders to be created\n            for _ in range(len(order_plan.order_actions)):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n            # ensure orders are properly created\n            open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol)\n            assert len(open_orders) == 10\n            assert sorted([f\"{o.origin_price}{o.side.value}\" for o in open_orders]) == sorted([\n                f\"{a.order_data.price}{a.order_data.side.value}\" for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n            ])\n            _get_reference_price_mock.reset_mock()\n            submit_trading_evaluation_mock.reset_mock()\n            _get_daily_volume_mock.reset_mock()\n\n            # 2. receive an update but orders are already in place: nothing to do\n            assert await producer._handle_market_making_orders(price, SYMBOL_MARKET, trigger_source, False) is True\n            _get_reference_price_mock.assert_called_once()\n            submit_trading_evaluation_mock.assert_not_called()\n            _get_reference_price_mock.reset_mock()\n            _get_daily_volume_mock.reset_mock()\n\n            # 3. receive an update, orders are already in place but force_full_refresh is True: refresh orders\n            assert await producer._handle_market_making_orders(price, SYMBOL_MARKET, trigger_source, True) is True\n            _get_reference_price_mock.assert_called_once()\n            _get_daily_volume_mock.assert_called_once()\n            submit_trading_evaluation_mock.assert_called_once()\n            assert submit_trading_evaluation_mock.mock_calls[0].kwargs[\"symbol\"] == symbol\n            data = submit_trading_evaluation_mock.mock_calls[0].kwargs[\"data\"]\n            order_plan: market_making_trading.OrdersUpdatePlan = data[market_making_trading.MarketMakingTradingModeConsumer.ORDER_ACTIONS_PLAN_KEY]\n            assert isinstance(order_plan, market_making_trading.OrdersUpdatePlan)\n            assert len(order_plan.order_actions) == 20\n            cancel_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CancelOrderAction)\n            ]\n            buy_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.BUY\n            ]\n            sell_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.SELL\n            ]\n            assert len(cancel_actions) == 10\n            assert len(buy_actions) == len(sell_actions) == 5\n            assert order_plan.cancelled == False\n            assert order_plan.cancellable == False # full replace is not cancellable\n            assert not order_plan.processed.is_set()\n            assert order_plan.trigger_source == trigger_source\n\n            # wait for orders to be created\n            for _ in range(len(order_plan.order_actions)):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n            # ensure orders are properly created\n            open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol)\n            assert len(open_orders) == 10\n            assert sorted([f\"{o.origin_price}{o.side.value}\" for o in open_orders]) == sorted([\n                f\"{a.order_data.price}{a.order_data.side.value}\" for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n            ])\n            _get_reference_price_mock.reset_mock()\n            submit_trading_evaluation_mock.reset_mock()\n\n\nasync def test_handle_market_making_orders_missing_funds_for_buy_orders():\n    symbol = \"BTC/USDT\"\n    async with _get_tools(symbol, additional_portfolio={\"USDT\": 15}) as (producer, consumer, exchange_manager):\n        price = decimal.Decimal(1000)\n        origin_submit_trading_evaluation = producer.submit_trading_evaluation\n        with mock.patch.object(\n            producer, \"submit_trading_evaluation\", mock.AsyncMock(side_effect=origin_submit_trading_evaluation)\n        ) as submit_trading_evaluation_mock, mock.patch.object(\n            producer, \"_get_reference_price\", mock.AsyncMock(return_value=price)\n        ) as _get_reference_price_mock, mock.patch.object(\n            producer, \"_get_daily_volume\", mock.Mock(return_value=(decimal.Decimal(1), decimal.Decimal(1000)))\n        ) as _get_daily_volume_mock:\n            trigger_source = \"ref_price\"\n            # 1. full replace as no order exist\n            assert await producer._handle_market_making_orders(price, SYMBOL_MARKET, trigger_source, False) is True\n            _get_reference_price_mock.assert_called_once()\n            _get_daily_volume_mock.assert_called_once()\n            submit_trading_evaluation_mock.assert_called_once()\n            data = submit_trading_evaluation_mock.mock_calls[0].kwargs[\"data\"]\n            order_plan: market_making_trading.OrdersUpdatePlan = data[market_making_trading.MarketMakingTradingModeConsumer.ORDER_ACTIONS_PLAN_KEY]\n            assert len(order_plan.order_actions) == 10\n            buy_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.BUY\n            ]\n            sell_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.SELL\n            ]\n            assert len(buy_actions) == len(sell_actions) == 5\n            assert order_plan.cancellable == False # full replace is not cancellable\n\n            # wait for orders to be created\n            for _ in range(len(order_plan.order_actions)):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n            # ensure orders are properly created\n            open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol)\n            # # only sell orders are created\n            assert sorted([f\"{o.origin_price}{o.side.value}\" for o in open_orders]) == sorted([\n                f\"{a.order_data.price}{a.order_data.side.value}\" for a in sell_actions\n            ])\n            _get_reference_price_mock.reset_mock()\n            submit_trading_evaluation_mock.reset_mock()\n\n            # 2. receive an update but orders are already in place: nothing to do\n            assert await producer._handle_market_making_orders(price, SYMBOL_MARKET, trigger_source, False) is True\n            _get_reference_price_mock.assert_called_once()\n            submit_trading_evaluation_mock.assert_not_called()\n            _get_reference_price_mock.reset_mock()\n            submit_trading_evaluation_mock.reset_mock()\n\n            # 3. an order got cancelled: recreate book\n            await exchange_manager.trader.cancel_order(open_orders[0])\n            assert await producer._handle_market_making_orders(price, SYMBOL_MARKET, trigger_source, False) is True\n            _get_reference_price_mock.assert_called_once()\n            submit_trading_evaluation_mock.assert_called_once()\n            data = submit_trading_evaluation_mock.mock_calls[0].kwargs[\"data\"]\n            order_plan: market_making_trading.OrdersUpdatePlan = data[market_making_trading.MarketMakingTradingModeConsumer.ORDER_ACTIONS_PLAN_KEY]\n            assert len(order_plan.order_actions) == 9\n            assert order_plan.cancellable == False # full replace is not cancellable\n            buy_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.BUY\n            ]\n            assert not buy_actions\n            sell_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CreateOrderAction)\n                   and a.order_data.side == trading_enums.TradeOrderSide.SELL\n            ]\n            assert len(sell_actions) == 5\n            cancel_actions = [\n                a for a in order_plan.order_actions\n                if isinstance(a, market_making_trading.CancelOrderAction)\n            ]\n            assert sorted([a.order for a in cancel_actions], key=lambda x: x.origin_price) == (\n                sorted(open_orders[1:], key=lambda x: x.origin_price)\n            )\n\n            # wait for orders to be cancelled and created\n            for _ in range(len(order_plan.order_actions)):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n            # ensure orders are properly created\n            open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol)\n            # # only sell orders are created\n            assert sorted([f\"{o.origin_price}{o.side.value}\" for o in open_orders]) == sorted([\n                f\"{a.order_data.price}{a.order_data.side.value}\" for a in sell_actions\n            ])\n            _get_reference_price_mock.reset_mock()\n            submit_trading_evaluation_mock.reset_mock()\n"
  },
  {
    "path": "Trading/Mode/market_making_trading_mode/tests/test_order_book_distribution.py",
    "content": "# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport pytest\n\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.constants as trading_constants\nimport tentacles.Trading.Mode.market_making_trading_mode.order_book_distribution as order_book_distribution\n\nBIDS_COUNT: int = 5\nASKS_COUNT: int = 5\nMIN_SPREAD: decimal.Decimal = decimal.Decimal(\"0.005\")\nMAX_SPREAD: decimal.Decimal = decimal.Decimal(\"0.05\")\n# binance symbol market extract\nSYMBOL_MARKET = {\n    'id': 'BTCUSDT', 'lowercaseId': 'btcusdt', 'symbol': 'BTC/USDT', 'base': 'BTC', 'quote': 'USDT',\n    'settle': None, 'baseId': 'BTC', 'quoteId': 'USDT', 'settleId': None, 'type': 'spot', 'spot': True,\n    'margin': True, 'swap': False, 'future': False, 'option': False, 'index': None, 'active': True,\n    'contract': False, 'linear': None, 'inverse': None, 'subType': None, 'taker': 0.001, 'maker': 0.001,\n    'contractSize': None, 'expiry': None, 'expiryDatetime': None, 'strike': None, 'optionType': None,\n    'precision': {'amount': 5, 'price': 2, 'cost': None, 'base': 1e-08, 'quote': 1e-08},\n    'limits': {\n        'leverage': {'min': None, 'max': None},\n        'amount': {'min': 1e-05, 'max': 9000.0},\n        'price': {'min': 0.01, 'max': 1000000.0},\n        'cost': {'min': 5.0, 'max': 9000000.0},\n        'market': {'min': 0.0, 'max': 107.1489592}\n    }, 'created': None,\n    'percentage': True, 'feeSide': 'get', 'tierBased': False\n}\n\n\n@pytest.fixture\ndef distribution():\n    return order_book_distribution.OrderBookDistribution(\n        BIDS_COUNT,\n        ASKS_COUNT,\n        MIN_SPREAD,\n        MAX_SPREAD,\n    )\n\n\ndef test_compute_distribution_base_config(distribution):\n    price = decimal.Decimal(\"50000.12\")\n    daily_base_volume = decimal.Decimal(\"10.1111111111111111111111111\")\n    daily_quote_volume = decimal.Decimal(\"450000.22222222222222222222222\")\n    # without available base / quote values\n    assert distribution is distribution.compute_distribution(\n        price, daily_base_volume, daily_quote_volume, SYMBOL_MARKET\n    )\n    assert len(distribution.asks) == ASKS_COUNT\n    assert len(distribution.bids) == BIDS_COUNT\n    # buy orders: lower than price, ordered from the highest to the lowest\n    assert [o.price for o in distribution.bids] == [\n        decimal.Decimal(str(p)) for p in [49875.11, 49593.86, 49312.61, 49031.36, 48750.11]\n    ]\n    highest_buy, lowest_buy = distribution.bids[0].price, distribution.bids[-1].price\n    lowest_sell, highest_sell = distribution.asks[0].price, distribution.asks[-1].price\n\n    # check spread\n    assert round(lowest_sell - highest_buy, 1) == round(price * MIN_SPREAD, 1)\n    assert round(highest_sell - lowest_buy, 1) == round(price * MAX_SPREAD, 1)\n\n    # check order book depth\n    provided_asks_volume_at_target_prices = sum(\n        o.amount for o in distribution.asks\n        if o.price <= price * (1 + order_book_distribution.TARGET_CUMULATED_VOLUME_PERCENT / decimal.Decimal(100))\n    )\n    min_target_base_volume = daily_base_volume * order_book_distribution.DAILY_TRADING_VOLUME_PERCENT / decimal.Decimal(100)\n    assert min_target_base_volume > decimal.Decimal(\"0\")\n    # use 99.9 of target value to account for decimal trunc\n    assert provided_asks_volume_at_target_prices >= min_target_base_volume * decimal.Decimal(\"0.999\")\n\n    quote_provided_bids_volume_at_target_prices = sum(\n        o.amount * o.price for o in distribution.bids\n        if o.price >= price * (1 - order_book_distribution.TARGET_CUMULATED_VOLUME_PERCENT / decimal.Decimal(100))\n    )\n    min_target_quote_volume = daily_quote_volume * order_book_distribution.DAILY_TRADING_VOLUME_PERCENT / decimal.Decimal(100)\n    assert min_target_quote_volume > decimal.Decimal(\"0\")\n    # use 99.9 of target value to account for decimal trunc\n    assert quote_provided_bids_volume_at_target_prices >= min_target_quote_volume * decimal.Decimal(\"0.999\")\n\n    # sell orders: higher than price, ordered from the lowest to the highest\n    assert [o.price for o in distribution.asks] == [\n        decimal.Decimal(str(p)) for p in [50125.12, 50406.37, 50687.62, 50968.87, 51250.12]\n    ]\n    assert [o.amount for o in distribution.bids] == [\n        decimal.Decimal(str(a)) for a in [0.03609, 0.03629, 0.0365, 0.03671, 0.03692]\n    ]\n    total_bid_size = sum(o.amount for o in distribution.bids)\n    assert total_bid_size\n    assert [o.amount for o in distribution.asks] == [\n        decimal.Decimal(str(a)) for a in [0.04044, 0.04044, 0.04044, 0.04044, 0.04044]\n    ]\n\n    trigger_source = \"ref_price\"\n    available_quote = distribution.get_ideal_total_volume(\n        trading_enums.TradeOrderSide.BUY, price, daily_base_volume, daily_quote_volume,\n    )\n    available_base = distribution.get_ideal_total_volume(\n        trading_enums.TradeOrderSide.SELL, price, daily_base_volume, daily_quote_volume,\n    )\n    # ensure distance computation is correct\n    distance_from_ideal_after_swaps = distribution.get_shape_distance_from(\n        distribution.bids + distribution.asks,\n        available_base, available_quote,\n        price, daily_base_volume, daily_quote_volume, trigger_source\n    )\n    assert 0 < distance_from_ideal_after_swaps < 0.006\n\n\ndef test_compute_distribution_base_config_with_max_available_amounts(distribution):\n    price = decimal.Decimal(\"50000.12\")\n    daily_base_volume = decimal.Decimal(\"10.1111111111111111111111111\")\n    daily_quote_volume = decimal.Decimal(\"450000.22222222222222222222222\")\n    available_base = decimal.Decimal(\"0.0945\")\n    available_quote = decimal.Decimal(\"199.01\")\n    # without available base / quote values\n    assert distribution is distribution.compute_distribution(\n        price, daily_base_volume, daily_quote_volume, SYMBOL_MARKET,\n        available_base=available_base,\n        available_quote=available_quote,\n    )\n    assert len(distribution.asks) == ASKS_COUNT\n    assert len(distribution.bids) == BIDS_COUNT\n    # price did not change\n    assert [o.price for o in distribution.bids] == [\n        decimal.Decimal(str(p)) for p in [49875.11, 49593.86, 49312.61, 49031.36, 48750.11]\n    ]\n    # price did not change\n    assert [o.price for o in distribution.asks] == [\n        decimal.Decimal(str(p)) for p in [50125.12, 50406.37, 50687.62, 50968.87, 51250.12]\n    ]\n    # volumes are reduced according available funds\n    assert [o.amount for o in distribution.bids] == [\n        decimal.Decimal(str(a)) for a in [0.00079, 0.0008, 0.0008, 0.00081, 0.00081]\n    ]\n    total_bid_size = sum(o.amount * o.price for o in distribution.bids)\n    assert (\n        available_quote * decimal.Decimal(\"0.99\") <= total_bid_size <= available_quote\n    )\n    # volumes are reduced according to budget\n    assert [o.amount for o in distribution.asks] == [\n        decimal.Decimal(str(a)) for a in [0.0189, 0.0189, 0.0189, 0.0189, 0.0189]\n    ]\n    total_ask_size = sum(o.amount for o in distribution.asks)\n    assert (\n        available_base * decimal.Decimal(\"0.9999\") <= total_ask_size <= available_base\n    )\n\n    trigger_source = \"ref_price\"\n    # ensure distance computation is correct\n    distance_from_ideal_after_swaps = distribution.get_shape_distance_from(\n        distribution.bids + distribution.asks,\n        available_base, available_quote,\n        price, daily_base_volume, daily_quote_volume, trigger_source\n    )\n    assert 0 < distance_from_ideal_after_swaps < 0.008\n\n\ndef test_infer_full_order_data_after_swaps(distribution):\n    # init ideal distribution\n    price = decimal.Decimal(\"50000.12\")\n    daily_base_volume = decimal.Decimal(\"10\")\n    daily_quote_volume = decimal.Decimal(\"450000\")\n    distribution.bids_count = 5\n    distribution.asks_count = 5\n    distribution.min_spread = decimal.Decimal(\"0.01\")\n    distribution.max_spread = decimal.Decimal(\"0.15\")\n    # without available base / quote values\n    distribution.compute_distribution(\n        price, daily_base_volume, daily_quote_volume, SYMBOL_MARKET\n    )\n    assert distribution.asks\n    assert distribution.bids\n    sorted_ideal_bids = order_book_distribution.get_sorted_sided_orders(distribution.bids, True)\n    sorted_ideal_asks = order_book_distribution.get_sorted_sided_orders(distribution.asks, True)\n    ideal_orders = sorted_ideal_bids + sorted_ideal_asks\n    available_base = decimal.Decimal(\"0.04\")\n    available_quote = decimal.Decimal(\"2000\")\n\n    # 1. ideal orders are open\n    updated_orders = distribution.infer_full_order_data_after_swaps(\n        ideal_orders, [], available_base, available_quote, price, daily_base_volume, daily_quote_volume\n    )\n    assert updated_orders == ideal_orders   # no scheduled change\n\n    # 2.a an ideal sell order got filled\n    existing_orders = sorted_ideal_bids + sorted_ideal_asks[1:]\n    updated_orders = distribution.infer_full_order_data_after_swaps(\n        existing_orders, [], available_base, available_quote, price, daily_base_volume, daily_quote_volume\n    )\n    assert len(updated_orders) == 10\n    assert updated_orders[0:5] == existing_orders[0:5]    # buy orders are identical\n    assert updated_orders[6:10] == existing_orders[5:9]    # sell orders are identical\n    # (except for 1st sell, which is not in existing orders)\n    assert round(updated_orders[5].price, 1) == round(sorted_ideal_asks[0].price, 1)\n\n\ndef test_validate_config(distribution):\n    distribution.validate_config()  # does not raise\n\n    # bids & asks count\n    distribution.asks_count = order_book_distribution.MAX_HANDLED_ASKS_ORDERS\n    distribution.validate_config()  # does not raise\n    distribution.asks_count = order_book_distribution.MAX_HANDLED_ASKS_ORDERS + 1\n    with pytest.raises(ValueError):\n        distribution.validate_config()\n    distribution.asks_count = order_book_distribution.MAX_HANDLED_ASKS_ORDERS\n    distribution.bids_count = order_book_distribution.MAX_HANDLED_BIDS_ORDERS + 1\n    with pytest.raises(ValueError):\n        distribution.validate_config()\n    distribution.bids_count = order_book_distribution.MAX_HANDLED_BIDS_ORDERS\n\n    # min spread\n    distribution.min_spread = distribution.max_spread\n    with pytest.raises(ValueError):\n        distribution.validate_config()\n    distribution.min_spread = distribution.max_spread + 1\n    with pytest.raises(ValueError):\n        distribution.validate_config()\n    distribution.min_spread = distribution.max_spread - 1\n\n    assert 50 > decimal.Decimal(\"2\") * order_book_distribution.TARGET_CUMULATED_VOLUME_PERCENT / trading_constants.ONE_HUNDRED\n    distribution.min_spread = decimal.Decimal(50)\n    with pytest.raises(ValueError):\n        distribution.validate_config()\n"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/__init__.py",
    "content": "from .remote_trading_signals_trading import RemoteTradingSignalsTradingMode"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/config/RemoteTradingSignalsTradingMode.json",
    "content": "{\n    \"trading_strategy\": \"\",\n    \"max_volume\": 50,\n    \"required_strategies\": []\n}"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"RemoteTradingSignalsTradingMode\"],\n  \"tentacles-requirements\": [\"remote_trading_signals_trading_mode\"]\n}"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/remote_trading_signals_trading.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\n\nimport octobot_commons.channels_name as channels_name\nimport octobot_commons.constants as common_constants\nimport octobot_commons.enums as common_enums\nimport octobot_commons.authentication as authentication\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_commons.signals as commons_signals\nimport async_channel.channels as channels\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.errors as errors\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.signals as trading_signals\nimport octobot_trading.personal_data as personal_data\nimport octobot_trading.modes.script_keywords as script_keywords\n\n\nclass RemoteTradingSignalsTradingMode(trading_modes.AbstractTradingMode):\n\n    def __init__(self, config, exchange_manager):\n        super().__init__(config, exchange_manager)\n        self.merged_symbol = None\n        self.last_signal_description = \"\"\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.UI.user_input(\n            common_constants.CONFIG_TRADING_SIGNALS_STRATEGY, common_enums.UserInputTypes.TEXT, \"\", inputs,\n            title=\"Trading strategy: identifier of the trading strategy to use.\"\n        )\n        self.UI.user_input(\n            RemoteTradingSignalsModeConsumer.MAX_VOLUME_PER_BUY_ORDER_CONFIG_KEY,\n            common_enums.UserInputTypes.FLOAT, 100, inputs,\n            min_val=0, max_val=100,\n            title=\"Maximum volume per buy order in % of quote symbol holdings (USDT for BTC/USDT).\",\n        )\n        self.UI.user_input(\n            RemoteTradingSignalsModeConsumer.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY_CONFIG_KEY,\n            common_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=\"Round to minimal size orders if missing funds according to signal. \"\n                  \"Used when copy signals require a volume that doesn't meet the minimal exchange order size.\"\n        )\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_current_state(self) -> (str, float):\n        producer_state = \"\" if self.producers[0].state in (None, trading_enums.EvaluatorStates.UNKNOWN) \\\n            else self.producers[0].state.name\n        return producer_state, self.last_signal_description\n\n    def get_mode_producer_classes(self) -> list:\n        return [RemoteTradingSignalsModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [RemoteTradingSignalsModeConsumer]\n\n    async def create_producers(self, auto_start) -> list:\n        producers = await super().create_producers(auto_start)\n        return producers + await self._subscribe_to_signal_feed()\n\n    async def _subscribe_to_signal_feed(self):\n        channel, created = await trading_signals.create_remote_trading_signal_channel_if_missing(\n            self.exchange_manager\n        )\n        if self.exchange_manager.is_backtesting:\n            # TODO: create and return producer simulator with this bot id\n            raise NotImplementedError(\"signal producer simulator is not implemented\")\n            return []\n        if created:\n            # only subscribe once to the signal channel\n            try:\n                await channel.subscribe_to_product_feed(\n                    self.trading_config[common_constants.CONFIG_TRADING_SIGNALS_STRATEGY]\n                )\n            except (authentication.AuthenticationRequired, authentication.AuthenticationError) as e:\n                self.logger.exception(e, True, f\"Error while subscribing to signal feed: {e}. Please sign in to \"\n                                               f\"your OctoBot account to receive trading signals\")\n            except Exception as e:\n                self.logger.exception(e, True, f\"Error while subscribing to signal feed: {e}. This trading mode won't \"\n                                               f\"be operating\")\n        return []\n\n    async def create_consumers(self) -> list:\n        consumers = await super().create_consumers()\n        signals_consumer = await channels.get_chan(\n            channels_name.OctoBotCommunityChannelsName.REMOTE_TRADING_SIGNALS_CHANNEL.value)\\\n            .new_consumer(\n                self._remote_trading_signal_callback,\n                identifier=self.trading_config[common_constants.CONFIG_TRADING_SIGNALS_STRATEGY],\n                symbol=self.symbol,\n                bot_id=self.bot_id\n            )\n        return consumers + [signals_consumer]\n\n    async def _remote_trading_signal_callback(self, identifier, exchange, symbol, version, bot_id, signal):\n        self.logger.info(f\"received signal: {signal}\")\n        await self.producers[0].signal_callback(signal)\n        self.logger.info(\"done\")\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n    @staticmethod\n    def is_backtestable():\n        return False\n\n    def is_following_trading_signals(self):\n        return True\n\n    async def stop(self) -> None:\n        self.logger.debug(\"Stopping trading mode: this should normally not be happening unless OctoBot is stopping\")\n        await super().stop()\n\n\nclass RemoteTradingSignalsModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    MAX_VOLUME_PER_BUY_ORDER_CONFIG_KEY = \"max_volume\"\n    ROUND_TO_MINIMAL_SIZE_IF_NECESSARY_CONFIG_KEY = \"round_to_minimal_size_if_necessary\"\n\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self.MAX_VOLUME_PER_BUY_ORDER = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config.get(self.MAX_VOLUME_PER_BUY_ORDER_CONFIG_KEY, 100)}\")\n        self.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = \\\n            self.trading_mode.trading_config.get(self.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY_CONFIG_KEY)\n\n    async def init_user_inputs(self, should_clear_inputs):\n        self.MAX_VOLUME_PER_BUY_ORDER = \\\n            decimal.Decimal(f\"{self.trading_mode.trading_config.get(self.MAX_VOLUME_PER_BUY_ORDER_CONFIG_KEY, 100)}\")\n        self.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = \\\n            self.trading_mode.trading_config.get(self.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY_CONFIG_KEY)\n\n    async def internal_callback(\n        self, trading_mode_name, cryptocurrency, symbol, time_frame, final_note, state,\n        data: commons_signals.Signal, dependencies=None\n    ):\n        \"\"\"\n        Override not to call self.create_order_if_possible to skip portfolio checks and handle signals directly\n        \"\"\"\n        try:\n            await self.handle_signal(symbol, data)\n        except errors.MissingMinimalExchangeTradeVolume:\n            self.logger.info(self.get_minimal_funds_error(symbol, final_note))\n        except Exception as e:\n            self.logger.exception(e, True, f\"Error when handling remote signal orders: {e}\")\n\n    async def handle_signal(self, symbol, data: commons_signals.Signal):\n        if data.topic == trading_enums.TradingSignalTopics.ORDERS.value:\n            # creates a new order (or multiple split orders), always check self.can_create_order() first.\n            await self._handle_signal_orders(symbol, data)\n        elif data.topic == trading_enums.TradingSignalTopics.POSITIONS.value:\n            await self._handle_positions_signal(symbol, data)\n        else:\n            self.logger.error(f\"Unhandled signal topic: {data.topic} (signal: {data})\")\n\n    async def _handle_positions_signal(self, symbol: str, signal: commons_signals.Signal):\n        action = signal.content.get(trading_enums.TradingSignalCommonsAttrs.ACTION.value)\n        if action == trading_enums.TradingSignalPositionsActions.EDIT.value:\n            await self._edit_position(symbol, signal)\n        else:\n            self.logger.error(f\"Unhandled signal action: {action} (signal: {signal})\")\n\n    async def _edit_position(self, symbol: str, signal: commons_signals.Signal):\n        leverage = signal.content.get(trading_enums.TradingSignalPositionsAttrs.LEVERAGE.value)\n        side = signal.content.get(trading_enums.TradingSignalPositionsAttrs.SIDE.value)\n        if side:\n            side = trading_enums.PositionSide(side)\n        if leverage is not None:\n            await self._set_leverage(symbol, decimal.Decimal(str(leverage)), side)\n\n    async def _handle_signal_orders(self, symbol: str, signal: commons_signals.Signal):\n        to_create_orders_descriptions, to_edit_orders_descriptions, \\\n            to_cancel_orders_descriptions, to_group_orders_descriptions = \\\n            self._parse_signal_orders(signal)\n        self._update_orders_according_to_config(to_edit_orders_descriptions)\n        self._update_orders_according_to_config(to_create_orders_descriptions)\n        await self._group_orders(to_group_orders_descriptions, symbol)\n        cancelled_count = await self._cancel_orders(to_cancel_orders_descriptions, symbol)\n        edited_count = await self._edit_orders(to_edit_orders_descriptions, symbol)\n        created_count = await self._create_orders(to_create_orders_descriptions, symbol)\n\n        self.trading_mode.last_signal_description = \\\n            f\"Last signal: {created_count} new order{'s' if created_count > 1 else ''}\"\n        # send_notification\n        if not self.exchange_manager.is_backtesting:\n            await self._send_alert_notification(symbol, created_count, edited_count, cancelled_count)\n\n    async def _group_orders(self, orders_descriptions, symbol):\n        groups = []\n        orders_by_group_id = {}\n        for order_description, order in self.get_open_order_from_description(orders_descriptions, symbol):\n            order_group = self._get_or_create_order_group(\n                order_description,\n                order_description[trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value])\n            if order_group not in groups:\n                groups.append(order_group)\n                orders_by_group_id[order_group.name] = []\n            order.add_to_order_group(order_group)\n            orders_by_group_id[order_group.name].append(order)\n\n        for group in groups:\n            # in futures, inactive orders are not necessary\n            if self.exchange_manager.trader.enable_inactive_orders and not self.exchange_manager.is_future:\n                await group.get_active_order_swap_strategy.apply_inactive_orders(orders_by_group_id[group.name])\n\n    async def _cancel_orders(self, orders_descriptions, symbol):\n        cancelled_count = 0\n        for _, order in self.get_open_order_from_description(orders_descriptions, symbol):\n            try:\n                await self._cancel_order_on_exchange(order)\n            except (errors.OrderCancelError, errors.UnexpectedExchangeSideOrderStateError) as err:\n                self.logger.warning(f\"Skipping order cancel: {err} ({err.__class__.__name__})\")\n            cancelled_count += 1\n        return cancelled_count\n\n    async def _edit_orders(self, orders_descriptions, symbol):\n        edited_count = 0\n        for order_description, order in self.get_open_order_from_description(orders_descriptions, symbol):\n            edited_price = order_description[trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value]\n            edited_stop_price = order_description[trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value]\n            edited_quantity, _, _ = await self._get_quantity_from_signal_percent(\n                order_description, order.side, symbol, order.reduce_only, True\n            )\n            await self._edit_order_on_exchange(\n                order,\n                edited_quantity=decimal.Decimal(edited_quantity) if edited_quantity else None,\n                edited_price=decimal.Decimal(edited_price) if edited_price else None,\n                edited_stop_price=decimal.Decimal(edited_stop_price) if edited_stop_price else None\n            )\n            edited_count += 1\n        return edited_count\n\n    async def _get_quantity_from_signal_percent(self, order_description, side, symbol, reduce_only, update_amount):\n        quantity_type, quantity = script_keywords.parse_quantity(\n            order_description[\n                trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value\n                if update_amount else trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value\n            ]\n        )\n        portfolio_type = common_constants.PORTFOLIO_TOTAL if quantity_type is script_keywords.QuantityType.PERCENT \\\n            else common_constants.PORTFOLIO_AVAILABLE\n        current_symbol_holding, current_market_holding, market_quantity, current_price, symbol_market = \\\n            await personal_data.get_pre_order_data(self.exchange_manager, symbol=symbol,\n                                                   timeout=trading_constants.ORDER_DATA_FETCHING_TIMEOUT,\n                                                   portfolio_type=portfolio_type)\n        if self.exchange_manager.is_future:\n            max_order_size, _ = personal_data.get_futures_max_order_size(\n                self.exchange_manager, symbol, side, current_price, reduce_only,\n                current_symbol_holding, market_quantity\n            )\n            position_percent = order_description[\n                trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value\n                if update_amount else trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value\n            ]\n            if position_percent is not None:\n                quantity_type, quantity = script_keywords.parse_quantity(position_percent)\n                if quantity_type in (\n                    script_keywords.QuantityType.POSITION_PERCENT,\n                    script_keywords.QuantityType.POSITION_PERCENT_ALIAS\n                ):\n                    open_position_size_val = \\\n                        self.exchange_manager.exchange_personal_data.positions_manager.get_symbol_position(\n                            symbol,\n                            trading_enums.PositionSide.BOTH\n                        ).size\n                    target_size = open_position_size_val * quantity / trading_constants.ONE_HUNDRED\n                    order_size = abs(target_size - open_position_size_val)\n                    return order_size, current_price, max_order_size\n                raise errors.InvalidArgumentError(f\"Unhandled position based quantity type: {position_percent}\")\n        else:\n            max_order_size = market_quantity if side is trading_enums.TradeOrderSide.BUY else current_symbol_holding\n        return max_order_size * quantity / trading_constants.ONE_HUNDRED, current_price, max_order_size\n\n    async def _bundle_order(self, order_description, to_create_orders, ignored_orders, bundled_with, fees_currency_side,\n                            created_groups, symbol):\n        chained_order = await self._create_order(order_description, symbol, created_groups, fees_currency_side)\n        try:\n            main_order = to_create_orders[bundled_with][0]\n            # always align bundled order quantity with the main order one\n            chained_order.update(chained_order.symbol, quantity=main_order.origin_quantity)\n            params = await self.exchange_manager.trader.bundle_chained_order_with_uncreated_order(\n                main_order, chained_order, chained_order.update_with_triggering_order_fees\n            )\n            to_create_orders[bundled_with][1].update(params)\n        except KeyError:\n            if bundled_with in ignored_orders:\n                self.logger.error(f\"Ignored order bundled to id {bundled_with}: \"\n                                  f\"associated master order has not been created\")\n\n    async def _chain_order(self, order_description, created_orders, ignored_orders, chained_to, fees_currency_side,\n                           created_groups, symbol, order_description_by_id):\n        failed_order_creation = False\n        try:\n            base_order = created_orders[chained_to]\n            if base_order is None:\n                # when an error occurred when creating the initial order\n                failed_order_creation = True\n                raise KeyError\n        except KeyError as e:\n            if chained_to in ignored_orders or failed_order_creation:\n                message = f\"Ignored order chained to id {chained_to}: associated master order has not been created\"\n                if failed_order_creation:\n                    self.logger.info(message)\n                else:\n                    self.logger.error(message)\n            else:\n                self.logger.error(\n                    f\"Ignored chained order from {order_description}. Chained orders have to be sent in the same \"\n                    f\"signal as the order they are chained to. Missing order with id: {e}.\")\n            return 0\n        desc_base_order_quantity = \\\n            order_description_by_id[chained_to][trading_enums.TradingSignalOrdersAttrs.QUANTITY.value]\n        desc_chained_order_quantity = order_description[trading_enums.TradingSignalOrdersAttrs.QUANTITY.value]\n        # compute target quantity based on the base order's description quantity to keep accurate %\n        target_quantity = decimal.Decimal(f\"{desc_chained_order_quantity}\") * base_order.origin_quantity / \\\n            decimal.Decimal(f\"{desc_base_order_quantity}\")\n        chained_order = await self._create_order(\n            order_description, symbol, created_groups, fees_currency_side, target_quantity=target_quantity\n        )\n        if chained_order.origin_quantity == trading_constants.ZERO:\n            self.logger.warning(f\"Ignored chained order: {chained_order}: not enough funds\")\n            return 0\n        await self.exchange_manager.trader.chain_order(\n            base_order, chained_order, chained_order.update_with_triggering_order_fees, False\n        )\n        if base_order.state is not None and base_order.is_filled() and chained_order.should_be_created():\n            try:\n                await personal_data.create_as_chained_order(chained_order)\n            except errors.OctoBotExchangeError as err:\n                # todo later on: handle locale error if necessary\n                self.logger.error(f\"Failed to create chained order: {chained_order}: {err}\")\n            return 1\n        return 0\n\n    async def _create_order(self, order_description, symbol, created_groups, fees_currency_side, target_quantity=None):\n        group = None\n        if group_id := order_description[trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value]:\n            group = created_groups[group_id]\n        side = trading_enums.TradeOrderSide(order_description[trading_enums.TradingSignalOrdersAttrs.SIDE.value])\n        reduce_only = order_description[trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value]\n        quantity, current_price, max_order_size = await self._get_quantity_from_signal_percent(\n            order_description, side, symbol, reduce_only, False\n        )\n        quantity = target_quantity or quantity\n        symbol_market = self.exchange_manager.exchange.get_market_status(symbol, with_fixer=False)\n        adapted_quantity = personal_data.decimal_adapt_quantity(symbol_market, quantity)\n        if adapted_quantity == trading_constants.ZERO:\n            if self.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY:\n                adapted_max_size = personal_data.decimal_adapt_quantity(symbol_market, max_order_size)\n                if adapted_max_size > trading_constants.ZERO:\n                    try:\n                        adapted_quantity = personal_data.get_minimal_order_amount(symbol_market)\n                        self.logger.info(f\"Minimal order amount reached, rounding to {adapted_quantity}\")\n                    except errors.NotSupported as e:\n                        self.logger.warning(f\"Impossible to round order to minimal order size: {e}.\")\n                else:\n                    funds_options = \" or increase leverage\" if self.exchange_manager.is_future else \"\"\n                    self.logger.warning(f\"Not enough funds to create minimal size order: current maximum order \"\n                                        f\"size={max_order_size}. Add funds{funds_options} to be able to trade.\")\n            else:\n                self.logger.info(\"Not enough funds to create order based on signal target amount. \"\n                                 \"Enable minimal size rounding to still trade in this situation. \"\n                                 \"Add funds or increase leverage to be able to trade in this setup.\")\n        price = order_description[trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value] \\\n            or order_description[trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value]\n        adapted_price = personal_data.decimal_adapt_price(symbol_market, decimal.Decimal(f\"{price}\"))\n        order_type = trading_enums.TraderOrderType(\n            order_description[trading_enums.TradingSignalOrdersAttrs.TYPE.value]\n        )\n        if order_type in (trading_enums.TraderOrderType.BUY_MARKET, trading_enums.TraderOrderType.SELL_MARKET):\n            # side param is not supported for these orders\n            side = None\n        associated_entries = order_description.get(\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value, None\n        )\n        trigger_above = order_description.get(trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value, None)\n        trailing_profile = None\n        if trailing_profile_details := order_description.get(\n            trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE.value\n        ):\n            trailing_profile = personal_data.create_trailing_profile(\n                personal_data.TrailingProfileTypes(\n                    order_description[trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE_TYPE.value],\n                ),\n                trailing_profile_details\n            )\n        is_active = order_description.get(trading_enums.TradingSignalOrdersAttrs.IS_ACTIVE.value, True)\n        active_trigger_price = (\n            None if order_description.get(trading_enums.TradingSignalOrdersAttrs.ACTIVE_TRIGGER_PRICE.value) is None\n            else decimal.Decimal(str(\n                order_description[trading_enums.TradingSignalOrdersAttrs.ACTIVE_TRIGGER_PRICE.value]\n            ))\n        )\n        active_trigger_above = order_description.get(trading_enums.TradingSignalOrdersAttrs.ACTIVE_TRIGGER_ABOVE.value)\n        cancel_policy = None\n        if cancel_policy_type := order_description.get(\n            trading_enums.TradingSignalOrdersAttrs.CANCEL_POLICY_TYPE.value\n        ):\n            cancel_policy = personal_data.create_cancel_policy(\n                cancel_policy_type,\n                order_description[trading_enums.TradingSignalOrdersAttrs.CANCEL_POLICY_KWARGS.value]\n            )\n        order = personal_data.create_order_instance(\n            trader=self.exchange_manager.trader,\n            order_type=order_type,\n            symbol=symbol,\n            current_price=current_price,\n            quantity=adapted_quantity,\n            price=adapted_price,\n            side=side,\n            trigger_above=trigger_above,\n            tag=order_description[trading_enums.TradingSignalOrdersAttrs.TAG.value],\n            order_id=order_description[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value],\n            group=group,\n            fees_currency_side=fees_currency_side,\n            reduce_only=reduce_only,\n            associated_entry_id=associated_entries[0] if associated_entries else None,\n            trailing_profile=trailing_profile,\n            is_active=is_active,\n            active_trigger_price=active_trigger_price,\n            active_trigger_above=active_trigger_above,\n            cancel_policy=cancel_policy\n        )\n        if associated_entries and len(associated_entries) > 1:\n            for associated_entry in associated_entries[1:]:\n                order.associate_to_entry(associated_entry)\n        order.update_with_triggering_order_fees = order_description.get(\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value, False\n        )\n        return order\n\n    def _get_or_create_order_group(self, order_description, group_id):\n        group_type = order_description[trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value]\n        group_class = tentacles_management.get_deep_class_from_parent_subclasses(group_type, personal_data.OrderGroup)\n        active_order_swap_strategy = None\n        if active_strategy_type := order_description.get(\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TYPE.value\n        ):\n            active_order_swap_strategy = tentacles_management.get_deep_class_from_parent_subclasses(\n                active_strategy_type, personal_data.ActiveOrderSwapStrategy\n            )(\n                swap_timeout=\n                    order_description[trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TIMEOUT.value],\n                trigger_price_configuration=\n                    order_description[trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TRIGGER_CONFIG.value]\n            )\n        return self.exchange_manager.exchange_personal_data.orders_manager.get_or_create_group(\n            group_class, group_id, active_order_swap_strategy=active_order_swap_strategy\n        )\n\n    async def _create_orders(self, orders_descriptions, symbol):\n        to_create_orders = {}   # dict of (orders, orders_param)\n        created_groups = {}\n        created_orders = {}\n        ignored_orders = set()\n        order_description_by_id = {\n            orders_description[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value]: orders_description\n            for orders_description in orders_descriptions\n        }\n        fees_currency_side = None\n        if self.exchange_manager.is_future:\n            fees_currency_side = self.exchange_manager.exchange.get_pair_future_contract(symbol)\\\n                .get_fees_currency_side()\n        for order_description in orders_descriptions:\n            if group_id := order_description[trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value]:\n                group = self._get_or_create_order_group(order_description, group_id)\n                await group.enable(False)\n                created_groups[group_id] = group\n            if order_description[trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value] is not None:\n                # bundled orders are created later on\n                continue\n            if order_description[trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value] is not None:\n                # chained orders are created later on\n                continue\n            created_order = await self._create_order(order_description, symbol, created_groups, fees_currency_side)\n            if created_order.origin_quantity == trading_constants.ZERO:\n                order_id = order_description[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value]\n                self.logger.error(f\"Impossible to create order: {created_order} \"\n                                  f\"(id: {order_id}): not enough funds on the account.\")\n                ignored_orders.add(order_id)\n            else:\n                to_create_orders[order_description[\n                    trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value]\n                ] = (created_order, {})\n        for order_description in orders_descriptions:\n            if bundled_with := order_description[trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value]:\n                await self._bundle_order(order_description, to_create_orders, ignored_orders, bundled_with,\n                                         fees_currency_side, created_groups, symbol)\n        # create orders\n        already_handled_order_ids = self.exchange_manager.exchange_personal_data.orders_manager\\\n            .get_all_active_and_pending_orders_id()\n        for order_id, order_with_param in to_create_orders.items():\n            if order_id in already_handled_order_ids:\n                self.logger.debug(f\"Ignored order with order id {order_id}: order already handled\")\n                continue\n            created_orders[order_id] = \\\n                await self._create_order_on_exchange(order_with_param[0], params=order_with_param[1])\n        # handle chained orders\n        created_chained_orders_count = 0\n        for order_description in orders_descriptions:\n            if (chained_to := order_description[trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value]) \\\n                    and order_description[trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value] is None:\n                order_id = \\\n                    order_description[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value]\n                if order_id in already_handled_order_ids:\n                    self.logger.debug(\n                        f\"Ignored order with order id {order_id}: order already handled\"\n                    )\n                    continue\n                created_chained_orders_count += \\\n                    await self._chain_order(order_description, created_orders, ignored_orders, chained_to,\n                                            fees_currency_side, created_groups, symbol, order_description_by_id)\n\n        for group in created_groups.values():\n            await group.enable(True)\n        return len(to_create_orders) + created_chained_orders_count\n\n    def get_open_order_from_description(self, order_descriptions, symbol):\n        found_orders = []\n        for order_description in order_descriptions:\n            # filter orders using order_id\n            if accurate_orders := [\n                (order_description, order)\n                for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol)\n                if order.order_id == order_description[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value]\n            ]:\n                found_orders += accurate_orders\n                continue\n            # 2nd chance: use order type and price as these are kept between bot restarts (loaded from exchange)\n            orders = [\n                (order_description, order)\n                for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol)\n                if (order.origin_price == order_description[trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value] or\n                    order.origin_price == order_description[trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value])\n                    and self._is_compatible_order_type(order, order_description)\n            ]\n            if orders:\n                found_orders += orders\n            else:\n                self.logger.error(\n                    f\"Order not found on {self.exchange_manager.exchange_name} open orders, \"\n                    f\"description: {order_description}\"\n                )\n        return found_orders\n\n    def _is_compatible_order_type(self, order, order_description):\n        side = order_description[trading_enums.TradingSignalOrdersAttrs.SIDE.value]\n        if not order.side.value == side:\n            return False\n        order_type = order_description[trading_enums.TradingSignalOrdersAttrs.TYPE.value]\n        return personal_data.TraderOrderTypeClasses[order_type] == order.__class__\n\n    def _parse_signal_orders(self, signal: commons_signals.Signal):\n        to_create_orders = []\n        to_edit_orders = []\n        to_cancel_orders = []\n        to_group_orders = []\n        for order_description in [signal.content] + self._get_nested_signal_order_descriptions(signal.content):\n            action = order_description.get(trading_enums.TradingSignalCommonsAttrs.ACTION.value)\n            if action == trading_enums.TradingSignalOrdersActions.CREATE.value:\n                to_create_orders.append(order_description)\n            elif action == trading_enums.TradingSignalOrdersActions.EDIT.value:\n                to_edit_orders.append(order_description)\n            elif action == trading_enums.TradingSignalOrdersActions.CANCEL.value:\n                to_cancel_orders.append(order_description)\n            elif action == trading_enums.TradingSignalOrdersActions.ADD_TO_GROUP.value:\n                to_group_orders.append(order_description)\n        return to_create_orders, to_edit_orders, to_cancel_orders, to_group_orders\n\n    def _get_nested_signal_order_descriptions(self, order_description):\n        order_descriptions = []\n        for nested_desc in order_description.get(trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value) or []:\n            order_descriptions.append(nested_desc)\n            # also explore multiple level nested signals\n            order_descriptions += self._get_nested_signal_order_descriptions(nested_desc)\n        return order_descriptions\n\n    def _update_orders_according_to_config(self, order_descriptions):\n        for order_description in order_descriptions:\n            self._update_according_to_config(order_description)\n\n    def _update_according_to_config(self, order_description):\n        # filter max buy order size\n        side = order_description.get(trading_enums.TradingSignalOrdersAttrs.SIDE.value, None)\n        if side is None:\n            found_orders = self.get_open_order_from_description([order_description], None)\n            try:\n                side = found_orders[0][1].side.value\n            except KeyError:\n                pass\n        if side is trading_enums.TradeOrderSide.BUY.value:\n            for key in (trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value,\n                        trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value):\n                self._update_quantity_according_to_config(order_description, key)\n\n    def _update_quantity_according_to_config(self, order_description, quantity_key):\n        quantity_type, quantity = script_keywords.parse_quantity(order_description[quantity_key])\n        if quantity is not None and quantity > self.MAX_VOLUME_PER_BUY_ORDER:\n            self.logger.warning(f\"Updating signal order {quantity_key} from {quantity}{quantity_type.value} \"\n                                f\"to {self.MAX_VOLUME_PER_BUY_ORDER}{quantity_type.value}\")\n            order_description[quantity_key] = f\"{self.MAX_VOLUME_PER_BUY_ORDER}{quantity_type.value}\"\n\n    async def _send_alert_notification(self, symbol, created, edited, cancelled):\n        try:\n            import octobot_services.api as services_api\n            import octobot_services.enums as services_enum\n            title = f\"New trading signal for {symbol}\"\n            messages = []\n            if created:\n                messages.append(f\"- Created {created} order{'s' if created > 1 else ''}\")\n            if edited:\n                messages.append(f\"- Edited {edited} order{'s' if edited > 1 else ''}\")\n            if cancelled:\n                messages.append(f\"- Cancelled {cancelled} order{'s' if cancelled > 1 else ''}\")\n            content = \"\\n\".join(messages)\n            await services_api.send_notification(services_api.create_notification(\n                content, title=title,\n                category=services_enum.NotificationCategory.TRADING_SCRIPT_ALERTS\n            ))\n        except ImportError as e:\n            self.logger.exception(e, True, f\"Impossible to send notification: {e}\")\n\n    # exchange methods: bypass trading modes api to avoid sending signals\n    async def _create_order_on_exchange(self, order, params):\n        return await self.exchange_manager.trader.create_order(order, params=params)\n\n    async def _cancel_order_on_exchange(self, order):\n        await self.exchange_manager.trader.cancel_order(order)\n\n    async def _edit_order_on_exchange(self, order, edited_quantity=None, edited_price=None, edited_stop_price=None):\n        await self.exchange_manager.trader.edit_order(\n            order,\n            edited_quantity=edited_quantity,\n            edited_price=edited_price,\n            edited_stop_price=edited_stop_price\n        )\n\n    async def _set_leverage(self, symbol: str, leverage: decimal.Decimal, side):\n        context = script_keywords.get_base_context(self.trading_mode, symbol=symbol)\n        await script_keywords.set_leverage(context, leverage, side=side)\n\n\nclass RemoteTradingSignalsModeProducer(trading_modes.AbstractTradingModeProducer):\n\n    def get_channels_registration(self):\n        # trading mode is waking up this producer directly from signal channel\n        return []\n\n    async def signal_callback(self, signal):\n        exchange_type = signal.content[trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value]\n        if exchange_type == exchanges.get_exchange_type(self.exchange_manager).value:\n            state = trading_enums.EvaluatorStates.UNKNOWN\n            symbol = (\n                signal.content.get(trading_enums.TradingSignalOrdersAttrs.SYMBOL.value)\n                or signal.content.get(trading_enums.TradingSignalPositionsAttrs.SYMBOL.value)\n            )\n            await self._set_state(\n                self.trading_mode.cryptocurrency,\n                symbol,\n                state, signal\n            )\n        else:\n            self.logger.error(f\"Incompatible signal exchange type: {exchange_type} \"\n                              f\"with current exchange: {self.exchange_manager}\")\n\n    async def _set_state(self, cryptocurrency: str, symbol: str, new_state, signal):\n        async with self.trading_mode_trigger():\n            self.state = new_state\n            self.logger.info(f\"[{symbol}] update state: {self.state.name}\")\n            # call orders creation from consumers\n            await self.submit_trading_evaluation(cryptocurrency=cryptocurrency,\n                                                 symbol=symbol,\n                                                 time_frame=None,\n                                                 final_note=self.final_eval,\n                                                 state=self.state,\n                                                 data=signal)\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        await super().stop()\n"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/resources/RemoteTradingSignalsTradingMode.md",
    "content": "RemoteTradingSignalsTradingMode trades using signals from community strategies you are subscribed to.\n\nNote: by default, if you don't meet the minimal exchange requirements for order size, \nthe smallest possible order size will be used. This can be disabled in options.\n\n_This trading mode supports PNL history when the signal emitter supports it as well._\n"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport os.path\nimport mock\nimport pytest\nimport pytest_asyncio\n\nimport octobot_backtesting.api as backtesting_api\nimport async_channel.util as channel_util\nimport async_channel.channels as channels\nimport octobot_commons.channels_name as channels_names\nimport octobot_commons.tests.test_config as test_config\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.signals as signals\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.signals as trading_signals\nimport tentacles.Trading.Mode as modes\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\nfrom tentacles.Trading.Mode.remote_trading_signals_trading_mode.remote_trading_signals_trading import \\\n    RemoteTradingSignalsTradingMode\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n\n@pytest_asyncio.fixture\nasync def local_trader(exchange_name=\"binance\", backtesting=None, symbol=\"BTC/USDT:USDT\"):\n    tentacles_manager_api.reload_tentacle_info()\n    exchange_manager = None\n    signal_channel = None\n    try:\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, exchange_name)\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = backtesting or await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[os.path.join(test_config.TEST_CONFIG_FOLDER,\n                                     \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel,\n                                            exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        mode = modes.RemoteTradingSignalsTradingMode(config, exchange_manager)\n        mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n        # avoid error when trying to connect to server signals\n        with mock.patch.object(RemoteTradingSignalsTradingMode, \"_subscribe_to_signal_feed\",\n                               new=mock.AsyncMock(return_value=[])) \\\n                as _subscribe_to_signal_feed_mock:\n            signal_channel, created = await trading_signals.create_remote_trading_signal_channel_if_missing(\n                exchange_manager\n            )\n            assert created is True\n            await mode.initialize()\n            # add mode to exchange manager so that it can be stopped and freed from memory\n            exchange_manager.trading_modes.append(mode)\n\n            # set BTC/USDT price at 1000 USDT\n            trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n            # let trading modes start\n            await asyncio_tools.wait_asyncio_next_cycle()\n            _subscribe_to_signal_feed_mock.assert_called_once()\n        yield mode.producers[0], mode.get_trading_mode_consumers()[0], trader\n    finally:\n        if exchange_manager is not None:\n            for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n                await backtesting_api.stop_importer(importer)\n            if exchange_manager.exchange.backtesting.time_updater is not None:\n                await exchange_manager.exchange.backtesting.stop()\n            await exchange_manager.stop()\n        if signal_channel is not None:\n            await signal_channel.stop()\n            channels.del_chan(channels_names.OctoBotCommunityChannelsName.REMOTE_TRADING_SIGNALS_CHANNEL.value)\n\n\nSIGNAL_TOPIC = trading_enums.TradingSignalTopics.ORDERS.value\n\n\n@pytest.fixture\ndef mocked_sell_limit_signal():\n    return signals.Signal(\n        SIGNAL_TOPIC,\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.SELL_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.3574085830652285%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 1010.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"46a0b2de-5b8f-4a39-89a0-137504f83dfc\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.BalancedTakeProfitAndStopOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TYPE.value: trading_personal_data.StopFirstActiveOrderSwapStrategy.__name__,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TIMEOUT.value: 3,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TRIGGER_CONFIG.value: trading_enums.ActiveOrderSwapTriggerPriceConfiguration.FILLING_PRICE.value,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_TRIGGER_PRICE.value: 21,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_TRIGGER_ABOVE.value: False,\n            trading_enums.TradingSignalOrdersAttrs.IS_ACTIVE.value: False,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5705d395-f970-45d9-9ba8-f63da17f17b2\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: True,\n        },\n        dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"456\")])\n    )\n\n\n@pytest.fixture\ndef mocked_sell_limit_signal_with_trailing_group():\n    return signals.Signal(\n        SIGNAL_TOPIC,\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.SELL_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.3574085830652285%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 1010.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"46a0b2de-5b8f-4a39-89a0-137504f83dfc\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.TrailingOnFilledTPBalancedOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5705d395-f970-45d9-9ba8-f63da17f17b2\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: True,\n        },\n        dependencies=trading_signals.get_orders_dependencies([])\n    )\n\n\n@pytest.fixture\ndef mocked_update_leverage_signal():\n    return signals.Signal(\n        trading_enums.TradingSignalTopics.POSITIONS.value,\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.EDIT.value,\n            trading_enums.TradingSignalPositionsAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalPositionsAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalPositionsAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.FUTURE.value,\n            trading_enums.TradingSignalPositionsAttrs.STRATEGY.value: \"plop strategy\",\n            trading_enums.TradingSignalPositionsAttrs.SIDE.value: None,\n            trading_enums.TradingSignalPositionsAttrs.LEVERAGE.value: 10,\n        }\n    )\n\n\n@pytest.fixture\ndef mocked_buy_limit_signal():\n    return signals.Signal(\n        SIGNAL_TOPIC,\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.BUY.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.BUY_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.3574085830652285%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 888.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5705d395-f970-45d9-9ba8-f63da17f17b2\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n        },\n        dependencies=trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"456\")])\n    )\n\n\n@pytest.fixture\ndef mocked_bundle_stop_loss_in_sell_limit_signal(mocked_sell_limit_signal):\n    mocked_sell_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.STOP_LOSS.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.356892%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 9990.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"46a0b2de-5b8f-4a39-89a0-137504f83dfc\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.BalancedTakeProfitAndStopOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5ad2a999-5ac2-47f0-9b69-c75a36f3858a\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: True,\n        }\n    )\n    return mocked_sell_limit_signal\n\n\n@pytest.fixture\ndef mocked_bundle_stop_loss_in_sell_limit_in_market_signal(mocked_sell_limit_signal, mocked_buy_market_signal):\n    trailing_profile = trading_personal_data.FilledTakeProfitTrailingProfile([\n        trading_personal_data.TrailingPriceStep(price, price, True)\n        for price in (10000, 12000, 13000)\n    ])\n    mocked_sell_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.STOP_LOSS.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.356892%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 9990.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"46a0b2de-5b8f-4a39-89a0-137504f83dfc\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.BalancedTakeProfitAndStopOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TYPE.value: trading_personal_data.StopFirstActiveOrderSwapStrategy.__name__,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TIMEOUT.value: 3,\n            trading_enums.TradingSignalOrdersAttrs.ACTIVE_SWAP_STRATEGY_TRIGGER_CONFIG.value: trading_enums.ActiveOrderSwapTriggerPriceConfiguration.FILLING_PRICE.value,\n            trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE_TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CANCEL_POLICY_TYPE.value: trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy.__name__,\n            trading_enums.TradingSignalOrdersAttrs.CANCEL_POLICY_KWARGS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5ad2a999-5ac2-47f0-9b69-c75a36f3858a\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: False,\n        }\n    )\n    mocked_buy_market_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        mocked_sell_limit_signal.content\n    )\n    return mocked_buy_market_signal\n\n\n@pytest.fixture\ndef mocked_bundle_trailing_stop_loss_in_sell_limit_in_market_signal(mocked_sell_limit_signal_with_trailing_group, mocked_buy_market_signal):\n    trailing_profile = trading_personal_data.FilledTakeProfitTrailingProfile([\n        trading_personal_data.TrailingPriceStep(price, price, True)\n        for price in (10000, 12000, 13000)\n    ])\n    mocked_sell_limit_signal_with_trailing_group.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.STOP_LOSS.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.356892%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 9990.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"46a0b2de-5b8f-4a39-89a0-137504f83dfc\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.TrailingOnFilledTPBalancedOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE_TYPE.value:\n                trading_personal_data.TrailingProfileTypes.FILLED_TAKE_PROFIT.value,\n            trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE.value: trailing_profile.to_dict(),\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5ad2a999-5ac2-47f0-9b69-c75a36f3858a\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: False,\n            trading_enums.TradingSignalOrdersAttrs.CANCEL_POLICY_TYPE.value: trading_personal_data.ExpirationTimeOrderCancelPolicy.__name__,\n            trading_enums.TradingSignalOrdersAttrs.CANCEL_POLICY_KWARGS.value: {\n                \"expiration_time\": 1000.0,\n            },\n        }\n    )\n    mocked_buy_market_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        mocked_sell_limit_signal_with_trailing_group.content\n    )\n    return mocked_buy_market_signal\n\n\n@pytest.fixture\ndef mocked_bundle_trigger_above_stop_loss_in_sell_limit_in_market_signal(mocked_sell_limit_signal, mocked_buy_market_signal):\n    mocked_sell_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.STOP_LOSS.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.356892%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 999999990.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"46a0b2de-5b8f-4a39-89a0-137504f83dfc\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.BalancedTakeProfitAndStopOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long exit (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"5ad2a999-5ac2-47f0-9b69-c75a36f3858a\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: False,\n        }\n    )\n    mocked_buy_market_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(\n        mocked_sell_limit_signal.content\n    )\n    return mocked_buy_market_signal\n\n\n@pytest.fixture\ndef mocked_buy_market_signal():\n    return signals.Signal(\n        SIGNAL_TOPIC,\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.BUY.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.BUY_MARKET.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.356892%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 1001.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"managed_order long entry (id: 143968020)\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"adc24701-573b-40dd-b6c9-3666cd22f33e\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n            trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: True,\n        }\n    )\n"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/tests/test_remote_trading_signals_trading_consumer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport pytest\nimport mock\n\nimport octobot_trading.api as trading_api\nimport octobot_commons.signals as commons_signals\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.errors as errors\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_services.api as services_api\nimport octobot_trading.modes.script_keywords as script_keywords\n\nfrom tentacles.Trading.Mode.remote_trading_signals_trading_mode.tests import local_trader, mocked_sell_limit_signal, \\\n    mocked_bundle_stop_loss_in_sell_limit_in_market_signal, mocked_buy_market_signal, mocked_buy_limit_signal, \\\n    mocked_update_leverage_signal, mocked_bundle_trigger_above_stop_loss_in_sell_limit_in_market_signal, \\\n    mocked_bundle_trailing_stop_loss_in_sell_limit_in_market_signal, mocked_sell_limit_signal_with_trailing_group\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_internal_callback(local_trader, mocked_sell_limit_signal, mocked_update_leverage_signal):\n    _, consumer, _ = local_trader\n    consumer.logger = mock.Mock(info=mock.Mock(), error=mock.Mock(), exception=mock.Mock())\n    with mock.patch.object(consumer, \"_handle_signal_orders\", new=mock.AsyncMock()) \\\n         as _handle_signal_orders_mock:\n        await consumer.internal_callback(\n            \"trading_mode_name\", \"cryptocurrency\", \"symbol\", \"time_frame\", \"final_note\", \"state\", mocked_sell_limit_signal\n        )\n        _handle_signal_orders_mock.assert_called_once_with(\"symbol\", mocked_sell_limit_signal)\n        consumer.logger.info.assert_not_called()\n        consumer.logger.error.assert_not_called()\n        consumer.logger.exception.assert_not_called()\n\n    with mock.patch.object(consumer, \"_handle_positions_signal\", new=mock.AsyncMock()) \\\n         as _handle_positions_signal_mock:\n        await consumer.internal_callback(\"trading_mode_name\", \"cryptocurrency\", \"symbol\", \"time_frame\", \"final_note\",\n                                         \"state\", mocked_update_leverage_signal)\n        _handle_positions_signal_mock.assert_called_once_with(\"symbol\", mocked_update_leverage_signal)\n        consumer.logger.info.assert_not_called()\n        consumer.logger.error.assert_not_called()\n        consumer.logger.exception.assert_not_called()\n\n    with mock.patch.object(consumer, \"_handle_signal_orders\",\n                           new=mock.AsyncMock(side_effect=errors.MissingMinimalExchangeTradeVolume)) \\\n         as _handle_signal_orders_mock:\n        await consumer.internal_callback(\"trading_mode_name\", \"cryptocurrency\", \"symbol/x\", \"time_frame\", \"final_note\",\n                                         \"state\", mocked_sell_limit_signal)\n        _handle_signal_orders_mock.assert_called_once_with(\"symbol/x\", mocked_sell_limit_signal)\n        consumer.logger.info.assert_called_once()\n        consumer.logger.error.assert_not_called()\n        consumer.logger.exception.assert_not_called()\n        consumer.logger.info.reset_mock()\n\n    with mock.patch.object(consumer, \"_handle_signal_orders\",\n                           new=mock.AsyncMock(side_effect=RuntimeError)) \\\n         as _handle_signal_orders_mock:\n        await consumer.internal_callback(\"trading_mode_name\", \"cryptocurrency\", \"symbol/x\", \"time_frame\", \"final_note\",\n                                         \"state\", mocked_sell_limit_signal)\n        _handle_signal_orders_mock.assert_called_once_with(\"symbol/x\", mocked_sell_limit_signal)\n        consumer.logger.info.assert_not_called()\n        consumer.logger.error.assert_not_called()\n        consumer.logger.exception.assert_called_once()\n\n    with mock.patch.object(consumer, \"_handle_signal_orders\",\n                           new=mock.AsyncMock(side_effect=RuntimeError)) as _handle_signal_orders_mock, \\\n        mock.patch.object(consumer, \"_handle_positions_signal\",\n            new=mock.AsyncMock(side_effect=RuntimeError)) \\\n         as _handle_positions_signal_mock:\n        mocked_sell_limit_signal.topic = \"plop\"\n        await consumer.internal_callback(\"trading_mode_name\", \"cryptocurrency\", \"symbol/x\", \"time_frame\", \"final_note\",\n                                         \"state\", mocked_sell_limit_signal)\n        _handle_signal_orders_mock.assert_not_called()\n        _handle_positions_signal_mock.assert_not_called()\n        consumer.logger.info.assert_not_called()\n        consumer.logger.error.assert_called_once()\n        consumer.logger.exception.assert_called_once()\n\n\nasync def test_handle_signal_orders(local_trader, mocked_bundle_stop_loss_in_sell_limit_in_market_signal):\n    _, consumer, trader = local_trader\n    symbol = mocked_bundle_stop_loss_in_sell_limit_in_market_signal.content[\n        trading_enums.TradingSignalOrdersAttrs.SYMBOL.value\n    ]\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    assert consumer.trading_mode.last_signal_description == \"\"\n    await consumer._handle_signal_orders(symbol, mocked_bundle_stop_loss_in_sell_limit_in_market_signal)\n    # ensure orders are created\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 2\n    # market order is filled, chained & bundled orders got created\n    assert isinstance(orders[0], trading_personal_data.StopLossOrder)\n    assert isinstance(orders[0].order_group, trading_personal_data.BalancedTakeProfitAndStopOrderGroup)\n    assert isinstance(orders[0].order_group.active_order_swap_strategy, trading_personal_data.StopFirstActiveOrderSwapStrategy)\n    assert orders[0].order_group.active_order_swap_strategy.swap_timeout == 3\n    assert orders[0].order_group.active_order_swap_strategy.trigger_price_configuration == trading_enums.ActiveOrderSwapTriggerPriceConfiguration.FILLING_PRICE.value\n    assert orders[0].trailing_profile is None\n    assert orders[0].update_with_triggering_order_fees is False\n    assert orders[0].origin_price == decimal.Decimal(\"9990\")\n    assert orders[0].trigger_above is False\n    assert orders[0].is_active is True\n    assert orders[0].active_trigger is None\n    assert isinstance(orders[0].cancel_policy, trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy)\n    assert isinstance(orders[1], trading_personal_data.SellLimitOrder)\n    assert orders[1].order_group is orders[0].order_group\n    assert orders[1].trailing_profile is None\n    assert orders[1].is_active is False\n    assert orders[1].active_trigger.trigger_price == decimal.Decimal(21)\n    assert orders[1].active_trigger.trigger_above is False\n    assert orders[1].update_with_triggering_order_fees is True\n    assert orders[1].trigger_above is True\n    assert orders[1].origin_quantity == decimal.Decimal(\"0.10713784\")   # initial quantity as\n    assert orders[1].cancel_policy is None\n    # update_with_triggering_order_fees is False\n    trades = list(exchange_manager.exchange_personal_data.trades_manager.trades.values())\n    assert len(trades) == 1\n    assert trades[0].trade_type, trading_enums.TraderOrderType.BUY_MARKET\n    assert trades[0].status is trading_enums.OrderStatus.FILLED\n    assert \"2\" in consumer.trading_mode.last_signal_description\n\n    # disable created order group so that changing their groups doesnt cancel them\n    await orders[0].order_group.enable(False)\n    # now edit, cancel orders and create a new one\n    # change StopLossOrder group and cancel SellLimitOrder\n    nested_edit_signal, cancel_signal, create_signal = _group_edit_cancel_create_order_signals(\n        orders[0].order_id, \"new_group_id\", trading_personal_data.OneCancelsTheOtherOrderGroup.__name__,\n        orders[0].order_id, \"3.356892%\", 2000,\n        orders[1].order_id\n    )\n    await consumer._handle_signal_orders(symbol, nested_edit_signal)\n    await consumer._handle_signal_orders(symbol, cancel_signal)\n    await consumer._handle_signal_orders(symbol, create_signal)\n    # ensure orders are created\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 2\n    # market order is filled, chained & bundled orders got created\n    assert isinstance(orders[0], trading_personal_data.StopLossOrder)\n    assert isinstance(orders[0].order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)    # not balance group anymore\n    assert orders[0].order_group.name == \"new_group_id\"\n    assert orders[0].origin_quantity == decimal.Decimal(\"0.3392821050783528672\")    # changed quantity according to fees\n    assert orders[0].origin_price == decimal.Decimal(\"2000\")    # changed price\n    assert isinstance(orders[1], trading_personal_data.BuyLimitOrder)   # not sell order (sell is cancelled)\n    trades = list(exchange_manager.exchange_personal_data.trades_manager.trades.values())\n    assert len(trades) == 2\n    assert trades[0].trade_type, trading_enums.TraderOrderType.BUY_MARKET\n    assert trades[1].trade_type, trading_enums.TraderOrderType.SellLimitOrder\n    assert trades[1].status is trading_enums.OrderStatus.CANCELED\n    assert \"1\" in consumer.trading_mode.last_signal_description\n\n\nasync def test_handle_signal_orders_trailing_stop_with_cancel_policy(\n    local_trader, mocked_bundle_trailing_stop_loss_in_sell_limit_in_market_signal\n):\n    _, consumer, trader = local_trader\n    symbol = mocked_bundle_trailing_stop_loss_in_sell_limit_in_market_signal.content[\n        trading_enums.TradingSignalOrdersAttrs.SYMBOL.value\n    ]\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    assert consumer.trading_mode.last_signal_description == \"\"\n    await consumer._handle_signal_orders(symbol, mocked_bundle_trailing_stop_loss_in_sell_limit_in_market_signal)\n    # ensure orders are created\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 2\n    # market order is filled, chained & bundled orders got created\n    assert isinstance(orders[0], trading_personal_data.StopLossOrder)\n    assert isinstance(orders[0].order_group, trading_personal_data.TrailingOnFilledTPBalancedOrderGroup)\n    # trailing profile is restored\n    assert orders[0].trailing_profile == trading_personal_data.FilledTakeProfitTrailingProfile([\n        trading_personal_data.TrailingPriceStep(price, price, True)\n        for price in (10000, 12000, 13000)\n    ])\n    assert orders[0].update_with_triggering_order_fees is False\n    assert orders[0].origin_price == decimal.Decimal(\"9990\")\n    assert orders[0].trigger_above is False\n    assert isinstance(orders[0].cancel_policy, trading_personal_data.ExpirationTimeOrderCancelPolicy)\n    assert orders[0].cancel_policy.expiration_time == 1000.0\n    assert isinstance(orders[1], trading_personal_data.SellLimitOrder)\n    assert orders[1].order_group is orders[0].order_group\n    assert orders[1].trailing_profile is None\n    assert orders[1].update_with_triggering_order_fees is True\n    assert orders[1].trigger_above is True\n    assert orders[1].origin_quantity == decimal.Decimal(\"0.10713784\")   # initial quantity as\n    assert orders[1].cancel_policy is None\n    # update_with_triggering_order_fees is False\n    trades = list(exchange_manager.exchange_personal_data.trades_manager.trades.values())\n    assert len(trades) == 1\n    assert trades[0].trade_type, trading_enums.TraderOrderType.BUY_MARKET\n    assert trades[0].status is trading_enums.OrderStatus.FILLED\n    assert \"2\" in consumer.trading_mode.last_signal_description\n\n\nasync def test_handle_signal_orders_trigger_above_stop_loss(local_trader, mocked_bundle_trigger_above_stop_loss_in_sell_limit_in_market_signal):\n    _, consumer, trader = local_trader\n    symbol = mocked_bundle_trigger_above_stop_loss_in_sell_limit_in_market_signal.content[\n        trading_enums.TradingSignalOrdersAttrs.SYMBOL.value\n    ]\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    assert consumer.trading_mode.last_signal_description == \"\"\n    await consumer._handle_signal_orders(symbol, mocked_bundle_trigger_above_stop_loss_in_sell_limit_in_market_signal)\n    # ensure orders are created\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 2\n    # market order is filled, chained & bundled orders got created\n    assert isinstance(orders[0], trading_personal_data.StopLossOrder)\n    assert isinstance(orders[0].order_group, trading_personal_data.BalancedTakeProfitAndStopOrderGroup)\n    assert orders[0].update_with_triggering_order_fees is False\n    assert orders[0].origin_price == decimal.Decimal(\"999999990\")\n    assert orders[0].trigger_above is True\n    assert isinstance(orders[1], trading_personal_data.SellLimitOrder)\n    assert orders[1].order_group is orders[0].order_group\n    assert orders[1].update_with_triggering_order_fees is True\n    assert orders[1].trigger_above is True\n    assert orders[1].origin_quantity == decimal.Decimal(\"0.10713784\")   # initial quantity as\n    # update_with_triggering_order_fees is False\n    trades = list(exchange_manager.exchange_personal_data.trades_manager.trades.values())\n    assert len(trades) == 1\n    assert trades[0].trade_type, trading_enums.TraderOrderType.BUY_MARKET\n    assert trades[0].status is trading_enums.OrderStatus.FILLED\n    assert \"2\" in consumer.trading_mode.last_signal_description\n\n\nasync def test_handle_signal_orders_no_triggering_order(\n    local_trader, mocked_bundle_stop_loss_in_sell_limit_in_market_signal\n):\n    _, consumer, trader = local_trader\n    symbol = mocked_bundle_stop_loss_in_sell_limit_in_market_signal.content[\n        trading_enums.TradingSignalOrdersAttrs.SYMBOL.value\n    ]\n    exchange_manager = trader.exchange_manager\n    await consumer._handle_signal_orders(symbol, mocked_bundle_stop_loss_in_sell_limit_in_market_signal)\n    # ensure orders are created\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 2\n    # market order is filled, chained & bundled orders got created\n    # same as test_handle_signal_orders: skip other asserts\n    assert orders[1].order_group is orders[0].order_group\n    assert orders[0].order_id in exchange_manager.exchange_personal_data.orders_manager.\\\n        get_all_active_and_pending_orders_id()\n    assert orders[1].order_id in exchange_manager.exchange_personal_data.orders_manager.\\\n        get_all_active_and_pending_orders_id()\n\n    # now edit, cancel orders and create a new one\n    # change StopLossOrder group and cancel SellLimitOrder\n    _, cancel_signal, _ = _group_edit_cancel_create_order_signals(\n        orders[0].order_id, \"new_group_id\", trading_personal_data.OneCancelsTheOtherOrderGroup.__name__,\n        orders[0].order_id, \"3.356892%\", 2000,\n        orders[1].order_id\n    )\n    cancel_signal.content[trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value] = \"0\"\n    await consumer._handle_signal_orders(symbol, cancel_signal)\n\n    port_cancel_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    # order 1 got cancelled, since it's grouped with order 0, both are cancelled\n    assert len(port_cancel_orders) ==0\n    assert orders[0].order_id not in exchange_manager.exchange_personal_data.orders_manager.\\\n        get_all_active_and_pending_orders_id()\n    assert orders[1].order_id not in exchange_manager.exchange_personal_data.orders_manager.\\\n        get_all_active_and_pending_orders_id()\n\n\nasync def test_handle_signal_orders_reduce_quantity_create_order(local_trader, mocked_buy_market_signal):\n    _, consumer, trader = local_trader\n    symbol = mocked_buy_market_signal.content[\n        trading_enums.TradingSignalOrdersAttrs.SYMBOL.value\n    ]\n    mocked_buy_market_signal.content[trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value] = \"75%\"\n    mocked_buy_market_signal.content[trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value] = None\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    assert consumer.trading_mode.last_signal_description == \"\"\n    await consumer._handle_signal_orders(symbol, mocked_buy_market_signal)\n    # market order is filled, chained & bundled orders got created\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 0\n    trades = list(exchange_manager.exchange_personal_data.trades_manager.trades.values())\n    assert len(trades) == 1\n    assert trades[0].trade_type, trading_enums.TraderOrderType.BUY_MARKET\n    # used 75% of funds\n    # can buy max 2, should buy 1.5, buy one because of config\n    assert trades[0].origin_quantity == decimal.Decimal(\"1\")\n    assert trades[0].origin_price == decimal.Decimal(\"1000\")\n\n\nasync def test_handle_signal_orders_reduce_quantity_edit_order(local_trader, mocked_buy_limit_signal):\n    _, consumer, trader = local_trader\n    symbol = mocked_buy_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.SYMBOL.value]\n    trading_api.force_set_mark_price(trader.exchange_manager, \"BTC/USDT:USDT\", 1000)\n    edit_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.EDIT.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: symbol,\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: \"80%\",\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: None,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: mocked_buy_limit_signal.content[\n                trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value\n            ],\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n        },\n    )\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    assert consumer.trading_mode.last_signal_description == \"\"\n    await consumer._handle_signal_orders(symbol, mocked_buy_limit_signal)\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 1\n    assert orders[0].origin_quantity == decimal.Decimal(\"0.10714817\")\n    await consumer._handle_signal_orders(symbol, edit_signal)\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 1\n    assert orders[0].origin_quantity == decimal.Decimal(\"1\")    # use 50% max as quantity (vs 80% in signal)\n\n\nasync def test_handle_signal_create_orders_not_enough_funds_using_min_amount(local_trader, mocked_buy_limit_signal):\n    _, consumer, trader = local_trader\n    symbol = mocked_buy_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.SYMBOL.value]\n    trading_api.force_set_mark_price(trader.exchange_manager, \"BTC/USDT:USDT\", 1000)\n    # too small amount for the current porfolio to handle within exchange rules\n    amount = \"0.00000001%\"\n    limit_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: symbol,\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.SELL_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: amount,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 20898.03,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 20600.31,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"98ea73a0-ed38-4fca-9744-ed7f80a2d3ef\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.OneCancelsTheOtherOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"12e7ad8f-10a1-4cd3-bf86-d972226bd079\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n        },\n    )\n    consumer.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = True\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    await consumer._handle_signal_orders(symbol, limit_signal)\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 1\n    order_1 = orders[0]\n    assert order_1.origin_quantity == decimal.Decimal(\"0.00001\")    # minimal amount according to exchange rules\n\n    consumer.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = False\n    # now disable minimal amount config\n    await consumer._handle_signal_orders(symbol, limit_signal)\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 1\n    assert orders[0] is order_1  # no order created\n\n    consumer.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = True\n    # re-enable minimal amount config\n    # same order id: no order created\n    await consumer._handle_signal_orders(symbol, limit_signal)\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 1\n    assert orders[0] is order_1  # no order created\n    # change order id not to skip creation\n    limit_signal.content[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value] = \"123\"\n    await consumer._handle_signal_orders(symbol, limit_signal)\n    orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n    assert len(orders) == 2\n    assert orders[0] is order_1  # no order created\n    assert orders[1].origin_quantity == decimal.Decimal(\"0.00001\")    # minimal amount according to exchange rules\n\n\nasync def test_handle_signal_create_orders_not_enough_available_funds_even_for_min_order(local_trader, mocked_buy_limit_signal):\n    _, consumer, trader = local_trader\n    symbol = \"BTC/USDT:USDT\"\n    trading_api.force_set_mark_price(trader.exchange_manager, \"BTC/USDT:USDT\", 1000)\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"BTC\"].available = trading_constants.ZERO\n    limit_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: symbol,\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.SELL_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"39.5865%a\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 20898.03,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 20600.31,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"98ea73a0-ed38-4fca-9744-ed7f80a2d3ef\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.OneCancelsTheOtherOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"12e7ad8f-10a1-4cd3-bf86-d972226bd079\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n        },\n    )\n    consumer.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = True\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    await consumer._handle_signal_orders(symbol, limit_signal)\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n\n\nasync def test_handle_signal_create_orders_not_enough_total_funds_even_for_min_order(local_trader, mocked_buy_limit_signal):\n    _, consumer, trader = local_trader\n    symbol = \"BTC/USDT:USDT\"\n    trading_api.force_set_mark_price(trader.exchange_manager, \"BTC/USDT:USDT\", 1000)\n    trader.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"BTC\"].total = trading_constants.ZERO\n    limit_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: symbol,\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.SELL_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"39.5865%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 20898.03,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 20600.31,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: \"98ea73a0-ed38-4fca-9744-ed7f80a2d3ef\",\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:\n                trading_personal_data.OneCancelsTheOtherOrderGroup.__name__,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"12e7ad8f-10a1-4cd3-bf86-d972226bd079\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n        },\n    )\n    consumer.ROUND_TO_MINIMAL_SIZE_IF_NECESSARY = True\n    exchange_manager = trader.exchange_manager\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n    await consumer._handle_signal_orders(symbol, limit_signal)\n    assert exchange_manager.exchange_personal_data.orders_manager.get_open_orders() == []\n\n\nasync def test_send_alert_notification(local_trader):\n    _, consumer, _ = local_trader\n    with mock.patch.object(services_api, \"send_notification\", mock.AsyncMock()) as send_notification_mock:\n        await consumer._send_alert_notification(\"BTC/USDT:USDT\", 42, 62, 78)\n        send_notification_mock.assert_called_once()\n        notification = send_notification_mock.mock_calls[0].args[0]\n        assert all(str(counter) in notification.text for counter in (42, 62, 78))\n\n        send_notification_mock.reset_mock()\n        await consumer._send_alert_notification(\"BTC/USDT:USDT\", 0, 0, 99)\n        send_notification_mock.assert_called_once()\n        notification = send_notification_mock.mock_calls[0].args[0]\n        assert \"99\" in notification.text\n        assert \"0\" not in notification.text\n\n\n# TODO add more unit hedge case tests when arch is validated\n\n\ndef _group_edit_cancel_create_order_signals(to_group_id, group_id, group_type,\n                                            to_edit_id, to_edit_target_amount, to_edit_price,\n                                            to_cancel_id):\n    nested_edit_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value:\n                trading_enums.TradingSignalOrdersActions.ADD_TO_GROUP.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.BUY_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.3574085830652285%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 800.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: group_id,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: group_type,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"second wave order\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: to_group_id,\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [\n                {\n                    trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.EDIT.value,\n                    trading_enums.TradingSignalOrdersAttrs.SIDE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n                    trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n                    trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n                    trading_enums.TradingSignalOrdersAttrs.TYPE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: 0,\n                    trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n                    trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: to_edit_target_amount,\n                    trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: to_edit_price,\n                    trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.TAG.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: to_edit_id,\n                    trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n                    trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n                },\n            ],\n        },\n    )\n    cancel_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CANCEL.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: None,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: to_cancel_id,\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n        },\n    )\n    create_signal = commons_signals.Signal(\n        \"moonmoon\",\n        {\n            trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,\n            trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,\n            trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: \"BTC/USDT:USDT\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: \"bybit\",\n            trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,\n            trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.BUY_LIMIT.value,\n            trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,\n            trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: \"5.3574085830652285%\",\n            trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,\n            trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 800.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,\n            trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,\n            trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,\n            trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: None,\n            trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value: None,\n            trading_enums.TradingSignalOrdersAttrs.TAG.value: \"second wave order\",\n            trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: \"aaaa-f970-45d9-9ba8-f63da17f17ba\",\n            trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: None,\n            trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: None,\n            trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],\n        }\n    )\n    return nested_edit_signal, cancel_signal, create_signal\n\n\nasync def test_handle_positions_signal(local_trader, mocked_update_leverage_signal):\n    _, consumer, trader = local_trader\n    symbol = mocked_update_leverage_signal.content[\n        trading_enums.TradingSignalPositionsAttrs.SYMBOL.value\n    ]\n    with mock.patch.object(consumer, \"_edit_position\", mock.AsyncMock()) as _edit_position_mock:\n        await consumer._handle_positions_signal(symbol, mocked_update_leverage_signal)\n        _edit_position_mock.assert_called_once_with(symbol, mocked_update_leverage_signal)\n        _edit_position_mock.reset_mock()\n\n        # unknown action\n        mocked_update_leverage_signal.content[trading_enums.TradingSignalCommonsAttrs.ACTION.value] = \"plop\"\n        await consumer._handle_positions_signal(symbol, mocked_update_leverage_signal)\n        _edit_position_mock.assert_not_called()\n\n\nasync def test_edit_position(local_trader, mocked_update_leverage_signal):\n    _, consumer, trader = local_trader\n    trader.exchange_manager.is_future = False\n    symbol = mocked_update_leverage_signal.content[\n        trading_enums.TradingSignalPositionsAttrs.SYMBOL.value\n    ]\n    with mock.patch.object(trader, \"set_leverage\", mock.AsyncMock()) as set_leverage_mock:\n        leverage = mocked_update_leverage_signal.content[\n            trading_enums.TradingSignalPositionsAttrs.LEVERAGE.value\n        ]\n        await consumer._handle_positions_signal(symbol, mocked_update_leverage_signal)\n        set_leverage_mock.assert_not_called()\n\n        trader.exchange_manager.is_future = True\n        await consumer._handle_positions_signal(symbol, mocked_update_leverage_signal)\n        set_leverage_mock.assert_called_once_with(symbol, None, decimal.Decimal(str(leverage)))\n        set_leverage_mock.reset_mock()\n\n        mocked_update_leverage_signal.content[\n            trading_enums.TradingSignalPositionsAttrs.SIDE.value\n        ] = trading_enums.PositionSide.LONG.value\n        await consumer._handle_positions_signal(symbol, mocked_update_leverage_signal)\n        set_leverage_mock.assert_called_once_with(symbol, trading_enums.PositionSide.LONG, decimal.Decimal(str(leverage)))\n\n    # do not propagate errors\n    with mock.patch.object(trader, \"set_leverage\", mock.AsyncMock(side_effect=NotImplementedError)) as set_leverage_mock:\n        await consumer._handle_positions_signal(symbol, mocked_update_leverage_signal)\n        set_leverage_mock.assert_called_once()"
  },
  {
    "path": "Trading/Mode/remote_trading_signals_trading_mode/tests/test_remote_trading_signals_trading_producer.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport mock\n\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nfrom tentacles.Trading.Mode.remote_trading_signals_trading_mode.tests import local_trader, \\\n    mocked_bundle_stop_loss_in_sell_limit_signal, mocked_sell_limit_signal, mocked_update_leverage_signal\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def test_signal_callback(local_trader, mocked_bundle_stop_loss_in_sell_limit_signal, mocked_update_leverage_signal):\n    producer, _, _ = local_trader\n    with mock.patch.object(producer, \"submit_trading_evaluation\", new=mock.AsyncMock()) \\\n         as submit_trading_evaluation_mock:\n        await producer.signal_callback(mocked_bundle_stop_loss_in_sell_limit_signal)\n        submit_trading_evaluation_mock.assert_called_once_with(\n            cryptocurrency=producer.trading_mode.cryptocurrency,\n            symbol=mocked_bundle_stop_loss_in_sell_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.SYMBOL.value],\n            time_frame=None,\n            final_note=trading_constants.ZERO,\n            state=trading_enums.EvaluatorStates.UNKNOWN,\n            data=mocked_bundle_stop_loss_in_sell_limit_signal\n        )\n        submit_trading_evaluation_mock.reset_mock()\n\n        # with incompatible exchange type\n        mocked_bundle_stop_loss_in_sell_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value] = trading_enums.ExchangeTypes.MARGIN.value\n        await producer.signal_callback(mocked_bundle_stop_loss_in_sell_limit_signal)\n        submit_trading_evaluation_mock.assert_not_called()\n\n        producer.exchange_manager.is_future = True\n        await producer.signal_callback(mocked_update_leverage_signal)\n        submit_trading_evaluation_mock.assert_called_once_with(\n            cryptocurrency=producer.trading_mode.cryptocurrency,\n            symbol=mocked_update_leverage_signal.content[trading_enums.TradingSignalPositionsAttrs.SYMBOL.value],\n            time_frame=None,\n            final_note=trading_constants.ZERO,\n            state=trading_enums.EvaluatorStates.UNKNOWN,\n            data=mocked_update_leverage_signal\n        )\n        submit_trading_evaluation_mock.reset_mock()\n"
  },
  {
    "path": "Trading/Mode/signal_trading_mode/__init__.py",
    "content": "from .signal_trading import SignalTradingMode"
  },
  {
    "path": "Trading/Mode/signal_trading_mode/config/SignalTradingMode.json",
    "content": "{\n    \"close_to_current_price_difference\": 0.02,\n    \"required_strategies\": [\n        \"MoveSignalsStrategyEvaluator\"\n    ],\n    \"max_currency_percent\": 100,\n    \"use_maximum_size_orders\": false,\n    \"use_prices_close_to_current_price\": false,\n    \"use_stop_orders\": true\n}"
  },
  {
    "path": "Trading/Mode/signal_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"SignalTradingMode\"],\n  \"tentacles-requirements\": [\"move_signals_strategy_evaluator\"]\n}"
  },
  {
    "path": "Trading/Mode/signal_trading_mode/resources/SignalTradingMode.md",
    "content": "SignalTradingMode is a trading mode adapted to liquid and relatively flat markets. \nIt will try to find reversals and trade them.  \n\nThis trading mode is using the daily trading mode orders system with adapted parameters.\n\nWarning: SignalTradingMode only works on liquid markets because the [Klinger Oscillator](https://www.investopedia.com/terms/k/klingeroscillator.asp) \nfrom MoveSignalsStrategyEvaluator the requires enough volume and candles continuity to be accurate."
  },
  {
    "path": "Trading/Mode/signal_trading_mode/signal_trading.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\n\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport tentacles.Trading.Mode.daily_trading_mode.daily_trading as daily_trading_mode\n\n\nclass SignalTradingMode(daily_trading_mode.DailyTradingMode):\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_current_state(self) -> (str, float):\n        return super().get_current_state()[0] if self.producers[0].state is None else self.producers[0].state.name, \\\n            self.producers[0].final_eval\n\n    def get_mode_producer_classes(self) -> list:\n        return [SignalTradingModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [SignalTradingModeConsumer]\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n\nclass SignalTradingModeConsumer(daily_trading_mode.DailyTradingModeConsumer):\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n\n        self.STOP_LOSS_ORDER_MAX_PERCENT = decimal.Decimal(str(0.99))\n        self.STOP_LOSS_ORDER_MIN_PERCENT = decimal.Decimal(str(0.95))\n\n        self.QUANTITY_MIN_PERCENT = decimal.Decimal(str(0.1))\n        self.QUANTITY_MAX_PERCENT = decimal.Decimal(str(0.9))\n\n        self.QUANTITY_MARKET_MIN_PERCENT = decimal.Decimal(str(0.5))\n        self.QUANTITY_MARKET_MAX_PERCENT = trading_constants.ONE\n        self.QUANTITY_BUY_MARKET_ATTENUATION = decimal.Decimal(str(0.2))\n\n        self.BUY_LIMIT_ORDER_MAX_PERCENT = decimal.Decimal(str(0.995))\n        self.BUY_LIMIT_ORDER_MIN_PERCENT = decimal.Decimal(str(0.99))\n\n\nclass SignalTradingModeProducer(daily_trading_mode.DailyTradingModeProducer):\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n\n        # If final_eval not is < X_THRESHOLD --> state = X\n        self.VERY_LONG_THRESHOLD = decimal.Decimal(str(-0.88))\n        self.LONG_THRESHOLD = decimal.Decimal(str(-0.4))\n        self.NEUTRAL_THRESHOLD = decimal.Decimal(str(0.4))\n        self.SHORT_THRESHOLD = decimal.Decimal(str(0.88))\n        self.RISK_THRESHOLD = decimal.Decimal(str(0.15))\n"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/__init__.py",
    "content": "from .staggered_orders_trading import StaggeredOrdersTradingMode"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/config/StaggeredOrdersTradingMode.json",
    "content": "{\n  \"required_strategies\": [],\n  \"pair_settings\": [\n    {\n    \"pair\": \"BTC/USDT\",\n    \"mode\": \"mountain\",\n    \"spread_percent\": 6,\n    \"increment_percent\": 3,\n    \"lower_bound\": 3000,\n    \"upper_bound\": 6000,\n    \"allow_instant_fill\": true,\n    \"operational_depth\": 100,\n    \"mirror_order_delay\": 0,\n    \"ignore_exchange_fees\": false,\n    \"use_existing_orders_only\": false\n    },\n  {\n    \"pair\": \"ADA/ETH\",\n    \"mode\": \"mountain\",\n    \"spread_percent\": 6,\n    \"increment_percent\": 3,\n    \"lower_bound\": 0.0003,\n    \"upper_bound\": 0.0007,\n    \"allow_instant_fill\": true,\n    \"operational_depth\": 50,\n    \"mirror_order_delay\": 0,\n    \"ignore_exchange_fees\": false,\n    \"use_existing_orders_only\": false\n  },\n  {\n    \"pair\": \"ETH/USDT\",\n    \"mode\": \"mountain\",\n    \"spread_percent\": 0.7,\n    \"increment_percent\": 0.3,\n    \"lower_bound\": 400,\n    \"upper_bound\": 500,\n    \"allow_instant_fill\": true,\n    \"operational_depth\": 50,\n    \"mirror_order_delay\": 0,\n    \"ignore_exchange_fees\": false,\n    \"use_existing_orders_only\": false\n  }\n  ]\n}"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"StaggeredOrdersTradingMode\"],\n  \"tentacles-requirements\": []\n}"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/resources/StaggeredOrdersTradingMode.md",
    "content": "StaggeredOrdersTrading is an advanced version of the GridTradingMode. \nIt places a large amount of buy and sell orders at fixed intervals, covering the order book from\nvery low prices to very high prices in a grid like fashion.  \nThe range (defined by lower & upper bounds) is supposed to cover all conceivable prices for as\nlong as the user intends to run the strategy, and this for each traded pair.\nThat could be from -100x to +100x (-99% to +10000%).  \nNote: the larger the covered range, the more orders and funds are required to execute the strategy. \n\nProfits will be made from price movements within the covered price area.  \nIt never \"sells at a loss\", but always at a profit, therefore OctoBot never cancels any orders when using the Staggered Orders Trading Mode.\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/staggered-orders-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=StaggeredOrdersTradingModeDocs\">\nfull Staggered Orders trading mode guide</a>.\n\n#### Changing configuration\n\nTo apply changes to the Staggered Orders Trading Mode settings, you will have to manually cancel orders and restart your OctoBot.  \nThis trading mode instantly places opposite side orders when an order is filled.  \nOctoBot also performs a check every 3 days to ensure the grid healthy state and create missing grid orders if any.\n\n#### Traded pairs\nOnly works with independent bases and quotes : ETH/USDT and ADA/BTC can be activated together but ETH/USDT\nand BTC/USDT can't be activated together for the same OctoBot instance since they are sharing the same symbol \n(here USDT).\n\n#### Funds allocation\nStaggered modes can be used to specify the way to allocate funds: modes are neutral, mountain, valley, sell slope and buy slope.\n\n_This trading mode supports PNL history._\n"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py",
    "content": "# pylint: disable=E701\n# Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport collections\nimport enum\nimport dataclasses\nimport math\nimport asyncio\nimport decimal\nimport typing\n\nimport async_channel.constants as channel_constants\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.data_util as data_util\nimport octobot_commons.signals as commons_signals\nimport octobot_trading.api as trading_api\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.exchanges.util as exchange_util\nimport octobot_trading.signals as signals\n\n\nclass StrategyModes(enum.Enum):\n    NEUTRAL = \"neutral\"\n    MOUNTAIN = \"mountain\"\n    VALLEY = \"valley\"\n    SELL_SLOPE = \"sell slope\"\n    BUY_SLOPE = \"buy slope\"\n    FLAT = \"flat\"\n\n\nclass ForceResetOrdersException(Exception):\n    pass\n\n\nclass TrailingAborted(Exception):\n    pass\n\n\nclass NoOrdersToTrail(Exception):\n    pass\n\n\nINCREASING = \"increasing_towards_current_price\"\nDECREASING = \"decreasing_towards_current_price\"\nSTABLE = \"stable_towards_current_price\"\nMULTIPLIER = \"multiplier\"\nMAX_TRAILING_PROCESS_DURATION = 5 * commons_constants.MINUTE_TO_SECONDS # enough to cancel & re-create orders\n\nONE_PERCENT_DECIMAL = decimal.Decimal(\"1.01\")\nTEN_PERCENT_DECIMAL = decimal.Decimal(\"1.1\")\n\nCREATED_ORDER_AVAILABLE_FUNDS_ALLOWED_RATIO = decimal.Decimal(\"0.97\")\n\n\nStrategyModeMultipliersDetails = {\n    StrategyModes.FLAT: {\n        MULTIPLIER: trading_constants.ZERO,\n        trading_enums.TradeOrderSide.BUY: STABLE,\n        trading_enums.TradeOrderSide.SELL: STABLE\n    },\n    StrategyModes.NEUTRAL: {\n        MULTIPLIER: decimal.Decimal(\"0.3\"),\n        trading_enums.TradeOrderSide.BUY: INCREASING,\n        trading_enums.TradeOrderSide.SELL: INCREASING\n    },\n    StrategyModes.MOUNTAIN: {\n        MULTIPLIER: trading_constants.ONE,\n        trading_enums.TradeOrderSide.BUY: INCREASING,\n        trading_enums.TradeOrderSide.SELL: INCREASING\n    },\n    StrategyModes.VALLEY: {\n        MULTIPLIER: trading_constants.ONE,\n        trading_enums.TradeOrderSide.BUY: DECREASING,\n        trading_enums.TradeOrderSide.SELL: DECREASING\n    },\n    StrategyModes.BUY_SLOPE: {\n        MULTIPLIER: trading_constants.ONE,\n        trading_enums.TradeOrderSide.BUY: DECREASING,\n        trading_enums.TradeOrderSide.SELL: INCREASING\n    },\n    StrategyModes.SELL_SLOPE: {\n        MULTIPLIER: trading_constants.ONE,\n        trading_enums.TradeOrderSide.BUY: INCREASING,\n        trading_enums.TradeOrderSide.SELL: DECREASING\n    }\n}\n\n\n@dataclasses.dataclass\nclass OrderData:\n    side: trading_enums.TradeOrderSide = None\n    quantity: decimal.Decimal = trading_constants.ZERO\n    price: decimal.Decimal = trading_constants.ZERO\n    symbol: str = 0\n    is_virtual: bool = True\n    associated_entry_id: str = None\n\n\nclass StaggeredOrdersTradingMode(trading_modes.AbstractTradingMode):\n    CONFIG_PAIR_SETTINGS = \"pair_settings\"\n    CONFIG_PAIR = \"pair\"\n    CONFIG_MODE = \"mode\"\n    CONFIG_SPREAD = \"spread_percent\"\n    CONFIG_INCREMENT_PERCENT = \"increment_percent\"\n    CONFIG_LOWER_BOUND = \"lower_bound\"\n    CONFIG_UPPER_BOUND = \"upper_bound\"\n    CONFIG_USE_EXISTING_ORDERS_ONLY = \"use_existing_orders_only\"\n    CONFIG_ALLOW_INSTANT_FILL = \"allow_instant_fill\"\n    CONFIG_OPERATIONAL_DEPTH = \"operational_depth\"\n    CONFIG_MIRROR_ORDER_DELAY = \"mirror_order_delay\"\n    CONFIG_ALLOW_FUNDS_REDISPATCH = \"allow_funds_redispatch\"\n    CONFIG_ENABLE_TRAILING_UP = \"enable_trailing_up\"\n    CONFIG_ENABLE_TRAILING_DOWN = \"enable_trailing_down\"\n    CONFIG_ORDER_BY_ORDER_TRAILING = \"order_by_order_trailing\"\n    CONFIG_FUNDS_REDISPATCH_INTERVAL = \"funds_redispatch_interval\"\n    COMPENSATE_FOR_MISSED_MIRROR_ORDER = \"compensate_for_missed_mirror_order\"\n    CONFIG_STARTING_PRICE = \"starting_price\"\n    CONFIG_BUY_FUNDS = \"buy_funds\"\n    CONFIG_SELL_FUNDS = \"sell_funds\"\n    CONFIG_SELL_VOLUME_PER_ORDER = \"sell_volume_per_order\"\n    CONFIG_BUY_VOLUME_PER_ORDER = \"buy_volume_per_order\"\n    CONFIG_IGNORE_EXCHANGE_FEES = \"ignore_exchange_fees\"\n    ENABLE_UPWARDS_PRICE_FOLLOW = \"enable_upwards_price_follow\"\n    CONFIG_DEFAULT_SPREAD_PERCENT = 1.5\n    CONFIG_DEFAULT_INCREMENT_PERCENT = 0.5\n    REQUIRE_TRADES_HISTORY = True   # set True when this trading mode needs the trade history to operate\n    SUPPORTS_INITIAL_PORTFOLIO_OPTIMIZATION = True  # set True when self._optimize_initial_portfolio is implemented\n    SUPPORTS_HEALTH_CHECK = False   # set True when self.health_check is implemented\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.UI.user_input(self.CONFIG_PAIR_SETTINGS, commons_enums.UserInputTypes.OBJECT_ARRAY,\n                           self.trading_config.get(self.CONFIG_PAIR_SETTINGS, None), inputs,\n                           item_title=\"Pair configuration\",\n                           other_schema_values={\"minItems\": 1, \"uniqueItems\": True},\n                           title=\"Configuration for each traded pairs.\")\n        self.UI.user_input(self.CONFIG_PAIR, commons_enums.UserInputTypes.TEXT, \"BTC/USDT\", inputs,\n                           other_schema_values={\"minLength\": 3, \"pattern\": commons_constants.TRADING_SYMBOL_REGEX},\n                           parent_input_name=self.CONFIG_PAIR_SETTINGS,\n                           title=\"Name of the traded pair.\"),\n        self.UI.user_input(\n            self.CONFIG_MODE, commons_enums.UserInputTypes.OPTIONS, StrategyModes.NEUTRAL.value, inputs,\n            options=list(mode.value for mode in StrategyModes),\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Mode: way to allocate funds in created orders.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_SPREAD, commons_enums.UserInputTypes.FLOAT,\n            self.CONFIG_DEFAULT_SPREAD_PERCENT, inputs,\n            min_val=0, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Spread: price difference between buy and sell orders: percent of the current price to use as \"\n                  \"spread (difference between highest buy and lowest sell). \"\n                  \"Example: enter 10 to use 10% of the current price as spread.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_INCREMENT_PERCENT, commons_enums.UserInputTypes.FLOAT,\n            self.CONFIG_DEFAULT_INCREMENT_PERCENT, inputs,\n            min_val=0, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Increment: price difference between grid orders: percent of the current price to use as increment \"\n                  \"between orders. Example: enter 3 to use 3% of the current price as increment. \"\n                  \"WARNING: this should be lower than the Spread value: profitability is close to \"\n                  \"Spread-Increment.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_LOWER_BOUND, commons_enums.UserInputTypes.FLOAT, 0.005, inputs,\n            min_val=0, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Lower bound: lower limit of the grid: minimum price to start placing buy orders from: lower \"\n                  \"limit of the grid. \"\n                  \"Example: a lower bound of 0.2 will create a grid covering a price down to 0.2.\"\n        )\n        self.UI.user_input(\n            self.CONFIG_UPPER_BOUND, commons_enums.UserInputTypes.FLOAT, 0.005, inputs,\n            min_val=0, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Upper bound: upper limit of the grid: maximum price to stop placing sell orders. \"\n                  \"Example: an upper bound of 1000 will create a grid covering up to a price for 1000.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_OPERATIONAL_DEPTH, commons_enums.UserInputTypes.INT, 50, inputs,\n            min_val=1, other_schema_values={\"exclusiveMinimum\": True},\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Operational depth: maximum number of orders to be maintained on exchange.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_MIRROR_ORDER_DELAY, commons_enums.UserInputTypes.FLOAT, 0, inputs,\n            min_val=0,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"[Optional] Mirror order delay: Seconds to wait for before creating a mirror order when an order \"\n                  \"is filled. This can generate extra profits on quick market moves.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_IGNORE_EXCHANGE_FEES, commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Ignore exchange fees: when checked, exchange fees won't be considered when creating mirror orders. \"\n                  \"When unchecked, a part of the total volume will be reduced to take exchange \"\n                  \"fees into account.\",\n        )\n        self.UI.user_input(\n            self.CONFIG_USE_EXISTING_ORDERS_ONLY, commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            parent_input_name=self.CONFIG_PAIR_SETTINGS,\n            title=\"Use existing orders only: when checked, new orders will only be created upon pre-existing orders \"\n                  \"fill. OctoBot won't create orders at startup: it will use the ones already on exchange instead. \"\n                  \"This mode allows staggered orders to operate on user created orders. \"\n                  \"Can't work on trading simulator.\",\n        )\n\n    def get_current_state(self) -> (str, float):\n        order = self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(self.symbol)\n        sell_count = len([o for o in order if o.side == trading_enums.TradeOrderSide.SELL])\n        buy_count = len(order) - sell_count\n        if buy_count > sell_count:\n            state = trading_enums.EvaluatorStates.LONG\n        elif buy_count < sell_count:\n            state = trading_enums.EvaluatorStates.SHORT\n        else:\n            state = trading_enums.EvaluatorStates.NEUTRAL\n        return state.name, f\"{buy_count} buy {sell_count} sell\"\n\n    def get_mode_producer_classes(self) -> list:\n        return [StaggeredOrdersTradingModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [StaggeredOrdersTradingModeConsumer]\n\n    async def create_consumers(self) -> list:\n        consumers = await super().create_consumers()\n        # order consumer: filter by symbol not be triggered only on this symbol's orders\n        order_consumer = await exchanges_channel.get_chan(trading_personal_data.OrdersChannel.get_name(),\n                                                          self.exchange_manager.id).new_consumer(\n            self._order_notification_callback,\n            symbol=self.symbol if self.symbol else channel_constants.CHANNEL_WILDCARD\n        )\n        return consumers + [order_consumer]\n\n    async def _order_notification_callback(self, exchange, exchange_id, cryptocurrency, symbol, order,\n                                           update_type, is_from_bot):\n        if (\n            order[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] == trading_enums.OrderStatus.FILLED.value\n            and order[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] in (\n                trading_enums.TradeOrderType.LIMIT.value\n            )\n            and is_from_bot\n        ):\n            await self.producers[0].order_filled_callback(order)\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n    def set_default_config(self):\n        raise RuntimeError(f\"Impossible to start {self.get_name()} without a valid configuration file.\")\n\n    async def single_exchange_process_health_check(self, chained_orders: list, tickers: dict) -> list:\n        created_orders = []\n        if await self._should_rebalance_orders():\n            target_asset = exchange_util.get_common_traded_quote(self.exchange_manager)\n            created_orders += await self.single_exchange_process_optimize_initial_portfolio([], target_asset, tickers)\n            for producer in self.producers:\n                await producer.trigger_staggered_orders_creation()\n        return created_orders\n\n    async def _should_rebalance_orders(self):\n        for producer in self.producers:\n            if producer.enable_upwards_price_follow:\n                # trigger rebalance when current price is beyond the highest sell order\n                if await producer.is_price_beyond_boundaries():\n                    return True\n        return False\n\n    async def single_exchange_process_optimize_initial_portfolio(\n        self, sellable_assets: list, target_asset: str, tickers: dict\n    ) -> list:\n        portfolio = self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio\n        producer = self.producers[0]\n        pair_bases = set()\n        # 1. cancel open orders\n        try:\n            cancelled_orders, part_1_dependencies = await self._cancel_associated_orders(producer, pair_bases)\n        except Exception as err:\n            self.logger.exception(err, True, f\"Error during portfolio optimization cancel orders step: {err}\")\n            cancelled_orders = []\n\n        # 2. convert assets to sell funds into target assets\n        try:\n            part_1_orders = await self._convert_assets_into_target(\n                producer, pair_bases, target_asset, set(sellable_assets), tickers, part_1_dependencies\n            )\n        except Exception as err:\n            self.logger.exception(\n                err, True, f\"Error during portfolio optimization convert into target step: {err}\"\n            )\n            part_1_orders = []\n\n        # 3. compute necessary funds for each configured_pairs\n        converted_quote_amount_per_symbol = self._get_converted_quote_amount_per_symbol(\n            portfolio, pair_bases, target_asset\n        )\n\n        # 4. buy assets\n        if converted_quote_amount_per_symbol == trading_constants.ZERO:\n            self.logger.warning(f\"No {target_asset} in portfolio after optimization.\")\n            part_2_orders = []\n        else:\n            part_2_dependencies = signals.get_orders_dependencies(part_1_orders)\n            part_2_orders = await self._buy_assets(\n                producer, pair_bases, target_asset, converted_quote_amount_per_symbol, tickers, part_2_dependencies\n            )\n\n        return [cancelled_orders, part_1_orders, part_2_orders]\n\n    async def _cancel_associated_orders(\n        self, producer, pair_bases\n    ) -> tuple[list, typing.Optional[commons_signals.SignalDependencies]]:\n        cancelled_orders = []\n        dependencies = commons_signals.SignalDependencies()\n        self.logger.info(f\"Optimizing portfolio: cancelling existing open orders on \"\n                         f\"{self.exchange_manager.exchange_config.traded_symbol_pairs}\")\n        for symbol in self.exchange_manager.exchange_config.traded_symbol_pairs:\n            if producer.get_symbol_trading_config(symbol) is not None:\n                pair_bases.add(symbol_util.parse_symbol(symbol).base)\n                for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(\n                    symbol=symbol\n                ):\n                    if not (order.is_cancelled() or order.is_closed()):\n                        cancelled, dependency = await self.cancel_order(order)\n                        if cancelled:\n                            dependencies.extend(dependency)\n                        cancelled_orders.append(order)\n        return cancelled_orders, (dependencies or None)\n\n    async def _convert_assets_into_target(\n        self, producer, pair_bases, common_quote, to_sell_assets, tickers, \n        dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ) -> list:\n        to_sell_assets = to_sell_assets.union(pair_bases)\n        self.logger.info(f\"Optimizing portfolio: selling {to_sell_assets} to buy {common_quote}\")\n        # need portfolio available to be up-to-date with cancelled orders\n        orders = await trading_modes.convert_assets_to_target_asset(\n            self, list(to_sell_assets), common_quote, tickers, dependencies=dependencies\n        )\n        if orders:\n            await asyncio.gather(\n                *[\n                    trading_personal_data.wait_for_order_fill(\n                        order, producer.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT, True\n                    ) for order in orders\n                ]\n            )\n        return orders\n\n    async def _buy_assets(\n        self, producer, pair_bases, common_quote, converted_quote_amount_per_symbol, tickers, \n        dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ) -> list:\n        created_orders = []\n        for base in pair_bases:\n            self.logger.info(\n                f\"Optimizing portfolio: buying {base} with \"\n                f\"{float(converted_quote_amount_per_symbol)} {common_quote}\"\n            )\n            try:\n                created_orders += await trading_modes.convert_asset_to_target_asset(\n                    self, common_quote, base, tickers,\n                    asset_amount=converted_quote_amount_per_symbol,\n                    dependencies=dependencies\n                )\n            except Exception as err:\n                self.logger.exception(err, True, f\"Error when creating order to buy {base}: {err}\")\n        if created_orders:\n            await asyncio.gather(\n                *[\n                    trading_personal_data.wait_for_order_fill(\n                        order, producer.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT, True\n                    ) for order in created_orders\n                ]\n            )\n        return created_orders\n\n    def _get_converted_quote_amount_per_symbol(self, portfolio, pair_bases, common_quote) -> decimal.Decimal:\n        trading_pairs_count = len(pair_bases)\n        # need portfolio available to be up-to-date with balancing orders\n        try:\n            kept_quote_amount = portfolio.portfolio[common_quote].available / decimal.Decimal(2)\n            return (\n                (portfolio.portfolio[common_quote].available - kept_quote_amount) /\n                decimal.Decimal(trading_pairs_count)\n            )\n        except KeyError:\n            # no common_quote in portfolio\n            return trading_constants.ZERO\n        except (decimal.DivisionByZero, decimal.InvalidOperation):\n            # no pair_bases\n            return trading_constants.ZERO\n\n\nclass StaggeredOrdersTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):\n    ORDER_DATA_KEY = \"order_data\"\n    CURRENT_PRICE_KEY = \"current_price\"\n    SYMBOL_MARKET_KEY = \"symbol_market\"\n    COMPLETING_TRAILING_KEY = \"completing_trailing\"\n\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self.skip_orders_creation = False\n\n    async def cancel_orders_creation(self):\n        self.logger.info(f\"Cancelling all orders creation for {self.trading_mode.symbol}\")\n        self.skip_orders_creation = True\n        try:\n            while not self.queue.empty():\n                await asyncio.sleep(0.1)\n        finally:\n            self.logger.info(f\"Orders creation fully cancelled for {self.trading_mode.symbol}\")\n            self.skip_orders_creation = False\n\n    async def create_new_orders(self, symbol, final_note, state, **kwargs):\n        # use dict default getter: can't afford missing data\n        data = kwargs[self.CREATE_ORDER_DATA_PARAM]\n        dependencies = kwargs[self.CREATE_ORDER_DEPENDENCIES_PARAM]\n        try:\n            if not self.skip_orders_creation:\n                order_data = data[self.ORDER_DATA_KEY]\n                current_price = data[self.CURRENT_PRICE_KEY]\n                symbol_market = data[self.SYMBOL_MARKET_KEY]\n                return await self.create_order(\n                    order_data, current_price, symbol_market, dependencies\n                )\n            else:\n                self.logger.info(f\"Skipped {data.get(self.ORDER_DATA_KEY, '')}\")\n        finally:\n            if data[self.COMPLETING_TRAILING_KEY]:\n                for producer in self.trading_mode.producers:\n                    # trailing process complete\n                    self.logger.info(f\"Completed {symbol} trailing process.\")\n                    producer.is_currently_trailing = False\n\n    async def create_order(\n        self, order_data, current_price, symbol_market, \n        dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ):\n        created_order = None\n        currency, market = symbol_util.parse_symbol(order_data.symbol).base_and_quote()\n        try:\n            base_available = trading_api.get_portfolio_currency(self.exchange_manager, currency).available\n            quote_available = trading_api.get_portfolio_currency(self.exchange_manager, market).available\n            selling = order_data.side == trading_enums.TradeOrderSide.SELL\n            quantity = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n                self.exchange_manager, order_data.symbol,\n                trading_enums.TraderOrderType.SELL_LIMIT if selling else trading_enums.TraderOrderType.BUY_LIMIT,\n                order_data.quantity, order_data.price, order_data.side\n            )\n            if selling and base_available < quantity and base_available > quantity * CREATED_ORDER_AVAILABLE_FUNDS_ALLOWED_RATIO:\n                quantity = quantity * CREATED_ORDER_AVAILABLE_FUNDS_ALLOWED_RATIO\n                self.logger.info(f\"Slightly adapted {order_data.symbol} {order_data.side.value} quantity to {quantity} to fit available funds\")\n            elif not selling:\n                cost = quantity * order_data.price\n                if quote_available < cost and quote_available > cost * CREATED_ORDER_AVAILABLE_FUNDS_ALLOWED_RATIO:\n                    quantity = quantity * CREATED_ORDER_AVAILABLE_FUNDS_ALLOWED_RATIO\n                    self.logger.info(f\"Slightly adapted {order_data.symbol} {order_data.side.value} quantity to {quantity} to fit available funds\")\n            for order_quantity, order_price in trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n                    quantity,\n                    order_data.price,\n                    symbol_market):\n                if selling:\n                    if base_available < order_quantity:\n                        self.logger.warning(\n                            f\"Skipping {order_data.symbol} {order_data.side.value} \"\n                            f\"[{self.exchange_manager.exchange_name}] order creation of \"\n                            f\"{order_quantity} at {float(order_price)}: \"\n                            f\"not enough {currency}: available: {base_available}, required: {order_quantity}\"\n                        )\n                        return []\n                elif quote_available < order_quantity * order_price:\n                    self.logger.warning(\n                        f\"Skipping {order_data.symbol} {order_data.side.value} \"\n                        f\"[{self.exchange_manager.exchange_name}] order creation of \"\n                        f\"{order_quantity} at {float(order_price)}: \"\n                        f\"not enough {market}: available: {quote_available}, required: {order_quantity * order_price}\"\n                    )\n                    return []\n                order_type = trading_enums.TraderOrderType.SELL_LIMIT if selling \\\n                    else trading_enums.TraderOrderType.BUY_LIMIT\n                current_order = trading_personal_data.create_order_instance(\n                    trader=self.exchange_manager.trader,\n                    order_type=order_type,\n                    symbol=order_data.symbol,\n                    current_price=current_price,\n                    quantity=order_quantity,\n                    price=order_price,\n                    associated_entry_id=order_data.associated_entry_id\n                )\n                # disable instant fill to avoid looping order fill in simulator\n                current_order.allow_instant_fill = False\n                created_order = await self.trading_mode.create_order(\n                    current_order, dependencies=dependencies\n                )\n            if not created_order:\n                self.logger.warning(\n                    f\"No order created for {order_data} (cost: {quantity * order_data.price}): \"\n                    f\"incompatible with exchange minimum rules. \"\n                    f\"Limits: {symbol_market[trading_enums.ExchangeConstantsMarketStatusColumns.LIMITS.value]}\"\n                )\n        except trading_errors.MissingFunds as e:\n            raise e\n        except Exception as e:\n            self.logger.exception(e, True, f\"Failed to create order : {e}. Order: {order_data}\")\n            return None\n        return [] if created_order is None else [created_order]\n\n\nclass StaggeredOrdersTradingModeProducer(trading_modes.AbstractTradingModeProducer):\n    FILL = 1\n    ERROR = 2\n    NEW = 3\n    min_quantity = \"min_quantity\"\n    max_quantity = \"max_quantity\"\n    min_cost = \"min_cost\"\n    max_cost = \"max_cost\"\n    min_price = \"min_price\"\n    max_price = \"max_price\"\n    PRICE_FETCHING_TIMEOUT = 60\n    MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT = 60\n    # health check once every 3 days\n    HEALTH_CHECK_INTERVAL_SECS = commons_constants.DAYS_TO_SECONDS * 3\n    # recent filled allowed time delay to consider as pending order_filled callback\n    RECENT_TRADES_ALLOWED_TIME = 10\n    # when True, orders creation/health check will be performed on start()\n    SCHEDULE_ORDERS_CREATION_ON_START = True\n    ORDERS_DESC = \"staggered\"\n    # keep track of available funds in order placement process to avoid spending multiple times\n    # the same funds due to async between producers and consumers and the possibility to trade multiple pairs with\n    # shared quote or base\n    AVAILABLE_FUNDS = {}\n    FUNDS_INCREASE_RATIO_THRESHOLD = decimal.Decimal(\"0.5\")  # ratio bellow with funds will be reallocated:\n    # used to track new funds and update orders accordingly\n    ALLOWED_MISSED_MIRRORED_ORDERS_ADAPT_DELTA_RATIO = decimal.Decimal(\"0.5\")\n\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        # no state for this evaluator: always neutral\n        self.state = trading_enums.EvaluatorStates.NEUTRAL\n        self.symbol = trading_mode.symbol\n        self.symbol_market = None\n        self.min_max_order_details = {}\n        fees = trading_api.get_fees(exchange_manager, self.symbol)\n        try:\n            self.max_fees = decimal.Decimal(str(max(fees[trading_enums.ExchangeConstantsMarketPropertyColumns.TAKER.value],\n                                                    fees[trading_enums.ExchangeConstantsMarketPropertyColumns.MAKER.value]\n                                                    )))\n        except TypeError as err:\n            # don't crash if fees are not available\n            market_status = self.exchange_manager.exchange.get_market_status(self.symbol, with_fixer=False)\n            self.logger.error(f\"Error reading fees for {self.symbol}: {err}. Market status: {market_status}\")\n            self.max_fees = decimal.Decimal(str(trading_constants.CONFIG_DEFAULT_FEES))\n        self.flat_increment = None\n        self.flat_spread = None\n        self.current_price = None\n        self.scheduled_health_check = None\n        self.sell_volume_per_order = self.buy_volume_per_order = self.starting_price = trading_constants.ZERO\n        self.mirror_orders_tasks = []\n        self.mirroring_pause_task = None\n        self.allow_order_funds_redispatch = False\n        self.enable_trailing_up = False\n        self.enable_trailing_down = False\n        self.use_order_by_order_trailing = True # enabled by default\n        self.funds_redispatch_interval = 24\n        self._expect_missing_orders = False\n        self._skip_order_restore_on_recently_closed_orders = True\n        self._use_recent_trades_for_order_restore = False\n        self._already_created_init_orders = False\n        self.compensate_for_missed_mirror_order = False\n\n        self.healthy = False\n\n        # used not to refresh orders when order_fill_callback is processing\n        self.lock = asyncio.Lock()\n\n        # staggered orders strategy parameters\n        self.symbol_trading_config = None\n\n        self.use_existing_orders_only = self.limit_orders_count_if_necessary = False\n        self.ignore_exchange_fees = True\n        self.enable_upwards_price_follow = True\n        self.mode = self.spread \\\n            = self.increment = self.operational_depth \\\n            = self.lowest_buy = self.highest_sell \\\n            = None\n        self.single_pair_setup = len(self.trading_mode.trading_config[self.trading_mode.CONFIG_PAIR_SETTINGS]) <= 1\n        self.mirror_order_delay = self.buy_funds = self.sell_funds = 0\n        self.allowed_mirror_orders = asyncio.Event()\n        self.allow_virtual_orders = True\n        self.health_check_interval_secs = self.__class__.HEALTH_CHECK_INTERVAL_SECS\n        self.healthy = False\n        self.is_currently_trailing = False\n        self.last_trailing_process_started_at = 0\n\n        try:\n            self._load_symbol_trading_config()\n        except KeyError as e:\n            error_message = f\"Impossible to start {self.ORDERS_DESC} orders for {self.symbol}: missing \" \\\n                            f\"configuration in trading mode config file. \"\n            self.logger.exception(e, True, error_message)\n            return\n        if self.symbol_trading_config is None:\n            configured_pairs = \\\n                [c[self.trading_mode.CONFIG_PAIR]\n                 for c in self.trading_mode.trading_config[self.trading_mode.CONFIG_PAIR_SETTINGS]]\n            self.logger.error(f\"No {self.ORDERS_DESC} orders configuration for trading pair: {self.symbol}. Add \"\n                              f\"this pair's details into your {self.ORDERS_DESC} orders configuration or disable this \"\n                              f\"trading pairs. Configured {self.ORDERS_DESC} orders pairs are\"\n                              f\" {', '.join(configured_pairs)}\")\n            return\n        self.already_errored_on_out_of_window_price = False\n\n        self.allowed_mirror_orders.set()\n        self.read_config()\n        self._check_params()\n        self._already_created_init_orders = True if self.use_existing_orders_only else False\n\n        self.logger.debug(f\"Loaded healthy config for {self.symbol}\")\n        self.healthy = True\n\n    def _load_symbol_trading_config(self) -> bool:\n        config = self.get_symbol_trading_config(self.symbol)\n        if config is None:\n            return False\n        self.symbol_trading_config = config\n        return True\n\n    def get_symbol_trading_config(self, symbol):\n        for config in self.trading_mode.trading_config[self.trading_mode.CONFIG_PAIR_SETTINGS]:\n            if config[self.trading_mode.CONFIG_PAIR] == symbol:\n                return config\n        return None\n\n    def read_config(self):\n        mode = \"\"\n        try:\n            mode = self.symbol_trading_config[self.trading_mode.CONFIG_MODE]\n            self.mode = StrategyModes(mode)\n        except ValueError as e:\n            self.logger.error(f\"Invalid {self.ORDERS_DESC} orders strategy mode: {mode} for {self.symbol}\"\n                              f\"supported modes are {[m.value for m in StrategyModes]}\")\n            raise e\n        self.spread = decimal.Decimal(str(self.symbol_trading_config[self.trading_mode.CONFIG_SPREAD] / 100))\n        self.increment = decimal.Decimal(str(self.symbol_trading_config[self.trading_mode.CONFIG_INCREMENT_PERCENT] / 100))\n        self.operational_depth = self.symbol_trading_config[self.trading_mode.CONFIG_OPERATIONAL_DEPTH]\n        self.lowest_buy = decimal.Decimal(str(self.symbol_trading_config[self.trading_mode.CONFIG_LOWER_BOUND]))\n        self.highest_sell = decimal.Decimal(str(self.symbol_trading_config[self.trading_mode.CONFIG_UPPER_BOUND]))\n        self.use_existing_orders_only = self.symbol_trading_config.get(\n            self.trading_mode.CONFIG_USE_EXISTING_ORDERS_ONLY,\n            self.use_existing_orders_only)\n        self.mirror_order_delay = self.symbol_trading_config.get(self.trading_mode.CONFIG_MIRROR_ORDER_DELAY,\n                                                                 self.mirror_order_delay)\n        self.buy_funds = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_BUY_FUNDS,\n                                                                            self.buy_funds)))\n        self.sell_funds = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_SELL_FUNDS,\n                                                                             self.sell_funds)))\n        self.ignore_exchange_fees = self.symbol_trading_config.get(self.trading_mode.CONFIG_IGNORE_EXCHANGE_FEES,\n                                                                   self.ignore_exchange_fees)\n        self.enable_upwards_price_follow = self.symbol_trading_config.get(\n            self.trading_mode.ENABLE_UPWARDS_PRICE_FOLLOW, self.enable_upwards_price_follow\n        )\n\n    async def start(self) -> None:\n        await super().start()\n        if StaggeredOrdersTradingModeProducer.SCHEDULE_ORDERS_CREATION_ON_START and self.healthy:\n            self.logger.debug(f\"Initializing orders creation\")\n            await self._ensure_staggered_orders_and_reschedule()\n\n    def get_extra_init_symbol_topics(self) -> typing.Optional[list]:\n        if self.exchange_manager.is_backtesting:\n            # disabled in backtesting as price might not be initialized at this point\n            return None\n        # required as trigger happens independently of price events for initial orders\n        return [commons_enums.InitializationEventExchangeTopics.PRICE.value]\n\n    async def stop(self):\n        if self.trading_mode is not None:\n            self.trading_mode.flush_trading_mode_consumers()\n        if self.scheduled_health_check is not None:\n            self.scheduled_health_check.cancel()\n        if self.mirroring_pause_task is not None and not self.mirroring_pause_task.done():\n            self.mirroring_pause_task.cancel()\n        for task in self.mirror_orders_tasks:\n            task.cancel()\n        if self.exchange_manager:\n            if self.exchange_manager.id in StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS:\n                # remove self.exchange_manager.id from available funds\n                StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(self.exchange_manager.id, None)\n        await super().stop()\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        # nothing to do: this is not a strategy related trading mode\n        pass\n\n    async def is_price_beyond_boundaries(self):\n        open_orders = self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(self.symbol)\n        price = await trading_personal_data.get_up_to_date_price(\n            self.exchange_manager, self.symbol, timeout=self.PRICE_FETCHING_TIMEOUT\n        )\n        max_order_price = max(\n            order.origin_price for order in open_orders\n        )\n        # price is above max order price\n        if max_order_price < price and self.enable_upwards_price_follow:\n            return True\n\n    def _schedule_order_refresh(self):\n        # schedule order creation / health check\n        asyncio.create_task(self._ensure_staggered_orders_and_reschedule())\n\n    async def _ensure_staggered_orders_and_reschedule(self):\n        if self.should_stop:\n            return\n        can_create_orders = (\n            not trading_api.get_is_backtesting(self.exchange_manager)\n            or trading_api.is_mark_price_initialized(self.exchange_manager, symbol=self.symbol)\n        ) and (\n            trading_api.get_portfolio(self.exchange_manager) != {}\n            or trading_api.is_trader_simulated(self.exchange_manager)\n        )\n        if can_create_orders:\n            try:\n                await self._ensure_staggered_orders()\n            except asyncio.TimeoutError:\n                can_create_orders = False\n        if not self.should_stop:\n            if can_create_orders:\n                # a None self.health_check_interval_secs disables health check\n                if self.health_check_interval_secs is not None:\n                    self.scheduled_health_check = asyncio.get_event_loop().call_later(\n                        self.health_check_interval_secs,\n                        self._schedule_order_refresh\n                    )\n            else:\n                self.logger.debug(f\"Can't yet create initialize orders for {self.symbol}\")\n                self.scheduled_health_check = asyncio.get_event_loop().call_soon(\n                    self._schedule_order_refresh\n                )\n\n    async def trigger_staggered_orders_creation(self):\n        if self.symbol_trading_config:\n            await self._ensure_staggered_orders(ignore_mirror_orders_only=True)\n        else:\n            self.logger.error(f\"No configuration for {self.symbol}\")\n\n    def start_mirroring_pause(self, delay):\n        if self.allowed_mirror_orders.is_set():\n            self.mirroring_pause_task = asyncio.create_task(self.stop_mirror_orders(delay))\n        else:\n            self.logger.info(f\"Cancelling previous {self.symbol} mirror order delay\")\n            self.mirroring_pause_task.cancel()\n            self.mirroring_pause_task = asyncio.create_task(self.stop_mirror_orders(delay))\n\n    async def stop_mirror_orders(self, delay):\n        self.logger.info(f\"Pausing {self.symbol} mirror orders creation for the next {delay} seconds\")\n        self.allowed_mirror_orders.clear()\n        await asyncio.sleep(delay)\n        self.allowed_mirror_orders.set()\n        self.logger.info(f\"Resuming {self.symbol} mirror orders creation after a {delay} seconds pause\")\n\n    async def _ensure_staggered_orders(\n        self, ignore_mirror_orders_only=False, ignore_available_funds=False, trigger_trailing=False\n    ):\n        _, _, _, self.current_price, self.symbol_market = await trading_personal_data.get_pre_order_data(\n            self.exchange_manager,\n            symbol=self.symbol,\n            timeout=self.PRICE_FETCHING_TIMEOUT\n        )\n        self.logger.debug(f\"{self.symbol} symbol_market initialized\")\n        await self.create_state(\n            self._get_new_state_price(), ignore_mirror_orders_only, ignore_available_funds, trigger_trailing\n        )\n\n    def _get_new_state_price(self):\n        return decimal.Decimal(str(self.current_price if self.starting_price == 0 else self.starting_price))\n\n    async def create_state(self, current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing):\n        if current_price is not None:\n            self._refresh_symbol_data(self.symbol_market)\n            async with self.get_lock(), self.trading_mode_trigger(skip_health_check=True):\n                if self.exchange_manager.trader.is_enabled:\n                    await self._handle_staggered_orders(\n                        current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing\n                    )\n                    self.logger.debug(f\"{self.symbol} orders updated on {self.exchange_name}\")\n\n\n    async def order_filled_callback(self, filled_order: dict):\n        # create order on the order side\n        new_order = self._create_mirror_order(filled_order)\n        self.logger.debug(f\"Creating mirror order: {new_order} after filled order: {filled_order}\")\n        filled_price = decimal.Decimal(str(\n            filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]\n        ))\n        if self.mirror_order_delay == 0 or trading_api.get_is_backtesting(self.exchange_manager):\n            await self._ensure_trailing_and_create_order_when_possible(new_order, filled_price)\n        else:\n            # create order after waiting time\n            self.mirror_orders_tasks.append(asyncio.get_event_loop().call_later(\n                self.mirror_order_delay,\n                asyncio.create_task,\n                self._ensure_trailing_and_create_order_when_possible(new_order, filled_price)\n            ))\n\n    def _create_mirror_order(self, filled_order: dict):\n        now_selling = filled_order[\n          trading_enums.ExchangeConstantsOrderColumns.SIDE.value\n        ] == trading_enums.TradeOrderSide.BUY.value\n        new_side = trading_enums.TradeOrderSide.SELL if now_selling else trading_enums.TradeOrderSide.BUY\n        associated_entry_id = filled_order[\n            trading_enums.ExchangeConstantsOrderColumns.ID.value\n        ] if now_selling else None  # don't double count PNL: only record entries on sell orders\n        if self.flat_increment is None:\n            details = \"self.flat_increment is unset\"\n            if self.symbol_market is None:\n                details = \"self.symbol_market is unset. Symbol mark price has not yet been initialized\"\n            self.logger.error(f\"Impossible to create symmetrical order for {self.symbol}: \"\n                              f\"{details}.\")\n            return\n        if self.flat_spread is None:\n            if not self.increment:\n                self.logger.error(f\"Impossible to create symmetrical order for {self.symbol}: \"\n                                  f\"self.flat_spread is None and self.increment is {self.increment}.\")\n            self.flat_spread = trading_personal_data.decimal_adapt_price(\n                self.symbol_market, self.spread * self.flat_increment / self.increment\n            )\n        mirror_price_difference = self.flat_spread - self.flat_increment\n        # try to get the order origin price to compute mirror order price\n        filled_price = decimal.Decimal(str(\n            filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]\n        ))\n        maybe_trade, maybe_order = self.exchange_manager.exchange_personal_data.get_trade_or_open_order(\n            filled_order[trading_enums.ExchangeConstantsOrderColumns.ID.value]\n        )\n        if maybe_trade:\n            # normal case\n            order_origin_price = maybe_trade.origin_price\n        elif maybe_order:\n            # should not happen but still handle it just in case\n            order_origin_price = maybe_order.origin_price\n        else:\n            # can't find order: default to filled price, even though it might be different from origin price\n            self.logger.warning(\n                f\"Computing mirror order price using filled order price: no associated trade or order has been \"\n                f\"found, this can lead to inconsistent order intervals (order: {filled_order})\"\n            )\n            order_origin_price = filled_price\n        price = order_origin_price + mirror_price_difference if now_selling else order_origin_price - mirror_price_difference\n\n        filled_volume = decimal.Decimal(str(filled_order[trading_enums.ExchangeConstantsOrderColumns.FILLED.value]))\n        fee = filled_order[trading_enums.ExchangeConstantsOrderColumns.FEE.value]\n        volume = self._compute_mirror_order_volume(now_selling, filled_price, price, filled_volume, fee)\n        checked_volume = self._get_available_funds_confirmed_order_volume(now_selling, price, volume)\n        return OrderData(new_side, checked_volume, price, self.symbol, False, associated_entry_id)\n\n    def _get_available_funds_confirmed_order_volume(self, selling, price, volume):\n        parsed_symbol = symbol_util.parse_symbol(self.symbol)\n        try:\n            if selling:\n                available_funds = trading_api.get_portfolio_currency(self.exchange_manager, parsed_symbol.base).available\n                return min(available_funds, volume)\n            else:\n                available_funds = trading_api.get_portfolio_currency(self.exchange_manager, parsed_symbol.quote).available\n                required_cost = price * volume\n                return min(available_funds, required_cost) / price\n        except decimal.DecimalException as err:\n            self.logger.exception(err, True, f\"Error when checking mirror order volume: {err}\")\n        return volume\n\n    def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, filled_volume, paid_fees: dict):\n        # use target volumes if set\n        if self.sell_volume_per_order != trading_constants.ZERO and now_selling:\n            return self.sell_volume_per_order\n        if self.buy_volume_per_order != trading_constants.ZERO and not now_selling:\n            return self.buy_volume_per_order\n        # otherwise: compute mirror volume\n        new_order_quantity = filled_volume\n        if not now_selling:\n            # buying => adapt order quantity\n            new_order_quantity = filled_price / target_price * filled_volume\n        # use max possible volume\n        if self.ignore_exchange_fees:\n            return new_order_quantity\n        # remove exchange fees\n        if paid_fees:\n            base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n            fees_in_base = trading_personal_data.get_fees_for_currency(paid_fees, base)\n            fees_in_base += trading_personal_data.get_fees_for_currency(paid_fees, quote) / filled_price\n            if fees_in_base == trading_constants.ZERO:\n                self.logger.debug(\n                    f\"Zero fees for trade on {self.symbol}\"\n                )\n        else:\n            self.logger.debug(\n                f\"No fees given to compute {self.symbol} mirror order size, using default ratio of {self.max_fees}\"\n            )\n            fees_in_base = new_order_quantity * self.max_fees\n        return new_order_quantity - fees_in_base\n\n    async def _ensure_trailing_and_create_order_when_possible(self, new_order, current_price):\n        if self._should_trigger_trailing(None, None, True):\n            # do not give current price as in this context, having only one-sided orders requires trailing\n            await self._ensure_staggered_orders(\n                trigger_trailing=True, ignore_available_funds=not self._should_lock_available_funds(True)\n            )\n        else:\n            async with self.get_lock():\n                await self._lock_portfolio_and_create_order_when_possible(new_order, current_price)\n\n    async def _lock_portfolio_and_create_order_when_possible(self, new_order, current_price):\n        await asyncio.wait_for(self.allowed_mirror_orders.wait(), timeout=None)\n        async with self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.lock:\n            await self._create_order(new_order, current_price, False, [])\n\n    def _should_trigger_trailing(\n        self,\n        orders: typing.Optional[list],\n        current_price: typing.Optional[decimal.Decimal],\n        trail_on_missing_orders: bool\n    ) -> bool:\n        if not (self.enable_trailing_up or self.enable_trailing_down):\n            return False\n        existing_orders = (\n            orders or self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(self.symbol)\n        )\n        buy_orders = sorted(\n            [order for order in existing_orders if order.side == trading_enums.TradeOrderSide.BUY],\n            key=lambda o: -o.origin_price\n        )\n        sell_orders = sorted(\n            [order for order in existing_orders if order.side == trading_enums.TradeOrderSide.SELL],\n            key=lambda o: o.origin_price\n        )\n        # 3 to allow trailing even if a few order from the other side have also been filled\n        one_sided_orders_trailing_threshold = self.operational_depth / 3\n        if self.enable_trailing_up and not sell_orders:\n            if len(buy_orders) < one_sided_orders_trailing_threshold and not trail_on_missing_orders:\n                (self.logger.info if trail_on_missing_orders else self.logger.warning)(\n                    f\"{self.symbol} trailing up process aborted: too many missing buy orders. \"\n                    f\"Only {len(buy_orders)} are online while configured total orders is {self.operational_depth}\"\n                )\n                return False\n            # only buy orders remaining: everything has been sold, trigger tailing up when enabled if price is\n            # beyond range\n            if current_price and buy_orders:\n                missing_orders_count = self.operational_depth - len(buy_orders)\n                price_delta = missing_orders_count * self.flat_increment\n                first_order = buy_orders[0]\n                approximated_highest_buy_price = first_order.origin_price + price_delta\n                if current_price >= approximated_highest_buy_price:\n                    # current price is beyond grid maximum buy price: trigger trailing\n                    return True\n                last_order = buy_orders[-1]\n                if last_order.origin_price - self.flat_increment < trading_constants.ZERO:\n                    # not all buy orders could have been created: trigger trailing as there is no way to check\n                    # the theoretical max price of the grid\n                    return len(buy_orders) >= self.operational_depth / 2 and current_price > first_order.origin_price\n            elif trail_on_missing_orders:\n                # needed for backtesting on-order-fill trailing trigger\n                return True\n        if self.enable_trailing_down and not buy_orders:\n            if len(sell_orders) < one_sided_orders_trailing_threshold and not trail_on_missing_orders:\n                (self.logger.info if trail_on_missing_orders else self.logger.warning)(\n                    f\"{self.symbol} trailing down process aborted: too many missing sell orders. \"\n                    f\"Only {len(sell_orders)} are online while configured total orders is {self.operational_depth}\"\n                )\n                return False\n            # only sell orders remaining: everything has been bought, trigger tailing up when enabled if price is\n            # beyond range\n            if current_price:\n                missing_orders_count = self.operational_depth - len(sell_orders)\n                price_delta = missing_orders_count * self.flat_increment\n                first_order = sell_orders[0]\n                approximated_lowest_sell_price = first_order.origin_price - price_delta\n                if current_price <= approximated_lowest_sell_price:\n                    # current price is beyond grid minimum sell price: trigger trailing\n                    return True\n            elif trail_on_missing_orders:\n                # needed for backtesting on-order-fill trailing trigger\n                return True\n        return False\n\n    def is_in_trailing_process(self) -> bool:\n        if self.is_currently_trailing:\n            last_trailing_duration = (\n                self.exchange_manager.exchange.get_exchange_current_time() - self.last_trailing_process_started_at\n            )\n            if last_trailing_duration > MAX_TRAILING_PROCESS_DURATION:\n                self.logger.info(f\"Removing trailing process flag: {MAX_TRAILING_PROCESS_DURATION} seconds reached\")\n                self.is_currently_trailing = False\n        return self.is_currently_trailing\n\n    async def _handle_staggered_orders(\n        self, current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing\n    ):\n        self._ensure_current_price_in_limit_parameters(current_price)\n        if not ignore_mirror_orders_only and self.use_existing_orders_only:\n            # when using existing orders only, no need to check existing orders (they can't be wrong since they are\n            # already on exchange): only initialize increment and order fill events will do the rest\n            self._set_increment_and_spread(current_price)\n        else:\n            async with self.producer_exchange_wide_lock(self.exchange_manager):\n                if trigger_trailing and self.is_in_trailing_process():\n                    self.logger.debug(\n                        f\"{self.symbol} on {self.exchange_name}: trailing signal ignored: \"\n                        f\"a trailing process is already running\"\n                    )\n                    return\n                # use exchange level lock to prevent funds double spend\n                buy_orders, sell_orders, triggering_trailing, create_order_dependencies = await self._generate_staggered_orders(\n                    current_price, ignore_available_funds, trigger_trailing\n                )\n                staggered_orders = self._merged_and_sort_not_virtual_orders(buy_orders, sell_orders)\n                await self._create_not_virtual_orders(\n                    staggered_orders, current_price, triggering_trailing, create_order_dependencies\n                )\n                if staggered_orders:\n                    self._already_created_init_orders = True\n    \n    def _should_lock_available_funds(self, trigger_trailing: bool) -> bool:\n        if trigger_trailing:\n            # don't lock available funds during order by order trailing\n            return not self.use_order_by_order_trailing\n        # don't lock available funds again after initial orders creation\n        return not self._already_created_init_orders\n\n    def _ensure_current_price_in_limit_parameters(self, current_price):\n        message = None\n        if self.highest_sell < current_price:\n            message = f\"The current price is hover the {self.ORDERS_DESC} orders boundaries for {self.symbol}: upper \" \\\n                      f\"bound is {self.highest_sell} and price is {current_price}. OctoBot can't trade using \" \\\n                      f\"these settings at this current price. Adjust your {self.ORDERS_DESC} orders upper bound \" \\\n                      f\"settings to use this trading mode.\"\n        if self.lowest_buy > current_price:\n            message = f\"The current price is bellow the {self.ORDERS_DESC} orders boundaries for {self.symbol}: \" \\\n                      f\"lower bound is {self.lowest_buy} and price is {current_price}. OctoBot can't trade using \" \\\n                      f\"these settings at this current price. Adjust your {self.ORDERS_DESC} orders \" \\\n                      f\"lower bound settings to use this trading mode.\"\n        if message is not None:\n            # Only log once in error, use warning of later messages.\n            self._log_window_error_or_warning(message, not self.already_errored_on_out_of_window_price)\n            self.already_errored_on_out_of_window_price = True\n        else:\n            self.already_errored_on_out_of_window_price = False\n\n    def _log_window_error_or_warning(self, message, using_error):\n        log_func = self.logger.error if using_error else self.logger.warning\n        log_func(message)\n\n    async def _generate_staggered_orders(\n        self, current_price, ignore_available_funds, trigger_trailing\n    ):\n        order_manager = self.exchange_manager.exchange_personal_data.orders_manager\n        interfering_orders_pairs = self._get_interfering_orders_pairs(order_manager.get_open_orders())\n        if interfering_orders_pairs:\n            self.logger.error(\n                f\"Impossible to create {self.ORDERS_DESC} orders for {self.symbol} with interfering orders using \"\n                f\"pair(s): {', '.join(interfering_orders_pairs)}. {self.ORDERS_DESC.capitalize()} orders require no \"\n                f\"other orders in both base and quote. Please use the Grid Trading Mode with configured Total funds\"\n                f\" trade with interfering orders.\"\n            )\n            return [], [], False, None\n        existing_orders = order_manager.get_open_orders(self.symbol)\n\n        sorted_orders = sorted(existing_orders, key=lambda order: order.origin_price)\n\n        recent_trades_time = trading_api.get_exchange_current_time(\n            self.exchange_manager) - self.RECENT_TRADES_ALLOWED_TIME\n        recently_closed_trades = trading_api.get_trade_history(self.exchange_manager, symbol=self.symbol,\n                                                               since=recent_trades_time)\n        recently_closed_trades = sorted(recently_closed_trades, key=lambda trade: trade.origin_price or trade.executed_price)\n        candidate_flat_increment = None\n        trigger_trailing = trigger_trailing or bool(\n            sorted_orders and self._should_trigger_trailing(sorted_orders, current_price, False)\n        )\n        next_step_dependencies = None\n        trailing_buy_orders = trailing_sell_orders = []\n        highest_buy = min(current_price, self.highest_sell)\n        lowest_sell = max(current_price, self.lowest_buy)\n        confirmed_trailing = False\n        if trigger_trailing:\n            # trailing has no initial dependencies here\n            _, __, trailing_buy_orders, trailing_sell_orders, next_step_dependencies = await self._prepare_trailing(\n                sorted_orders, recently_closed_trades, self.lowest_buy, highest_buy, lowest_sell, self.highest_sell, \n                current_price, None\n            )\n            confirmed_trailing = True\n            # trailing will cancel all orders: set state to NEW with no existing order\n            missing_orders, state, sorted_orders = None, self.NEW, []\n        else:\n            missing_orders, state, candidate_flat_increment = self._analyse_current_orders_situation(\n                sorted_orders, recently_closed_trades, self.lowest_buy, self.highest_sell, current_price\n            )\n        self._set_increment_and_spread(current_price, candidate_flat_increment)\n        try:\n            if trailing_buy_orders or trailing_sell_orders:\n                buy_orders = trailing_buy_orders\n                sell_orders = trailing_sell_orders\n            else:\n                buy_orders = self._create_orders(self.lowest_buy, highest_buy, trading_enums.TradeOrderSide.BUY, sorted_orders,\n                                                current_price, missing_orders, state, self.buy_funds, ignore_available_funds,\n                                                recently_closed_trades)\n                sell_orders = self._create_orders(lowest_sell, self.highest_sell, trading_enums.TradeOrderSide.SELL, sorted_orders,\n                                                current_price, missing_orders, state, self.sell_funds, ignore_available_funds,\n                                                recently_closed_trades)\n                if state is self.FILL:\n                    self._ensure_used_funds(buy_orders, sell_orders, sorted_orders, recently_closed_trades)\n                elif state is self.NEW:\n                    if trigger_trailing and not (buy_orders or sell_orders):\n                        self.logger.error(f\"Unhandled situation: no orders created for {self.symbol} with trigger_trailing={trigger_trailing}\")\n            create_order_dependencies = next_step_dependencies\n        except ForceResetOrdersException:\n            buy_orders, sell_orders, state, create_order_dependencies = await self._reset_orders(\n                sorted_orders, self.lowest_buy, highest_buy, lowest_sell, self.highest_sell,\n                current_price, ignore_available_funds, next_step_dependencies\n            )\n\n        if state == self.NEW:\n            self._set_virtual_orders(buy_orders, sell_orders, self.operational_depth)\n\n        return buy_orders, sell_orders, confirmed_trailing, create_order_dependencies\n\n    async def _reset_orders(\n        self, sorted_orders, lowest_buy, highest_buy, lowest_sell, highest_sell, \n        current_price, ignore_available_funds, \n        dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ) -> tuple[list, list, int, typing.Optional[commons_signals.SignalDependencies]]:\n        self.logger.info(\"Resetting orders\")\n        cancelled_and_dependency_results = await asyncio.gather(*(self._cancel_open_order(order, dependencies) for order in sorted_orders))\n        orders_dependencies = commons_signals.SignalDependencies()\n        for result in cancelled_and_dependency_results:\n            if result[0] and result[1] is not None:\n                orders_dependencies.extend(result[1])\n        self._reset_available_funds()\n        state = self.NEW\n        buy_orders = self._create_orders(\n            lowest_buy, highest_buy, trading_enums.TradeOrderSide.BUY, sorted_orders,\n            current_price, [], state, self.buy_funds, ignore_available_funds, []\n        )\n        sell_orders = self._create_orders(\n            lowest_sell, highest_sell, trading_enums.TradeOrderSide.SELL, sorted_orders,\n            current_price, [], state, self.sell_funds, ignore_available_funds, []\n        )\n        return buy_orders, sell_orders, state, (orders_dependencies or None)\n\n    def _reset_available_funds(self):\n        base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        self._set_initially_available_funds(\n            base,\n            trading_api.get_portfolio_currency(self.exchange_manager, base).available,\n        )\n        self._set_initially_available_funds(\n            quote,\n            trading_api.get_portfolio_currency(self.exchange_manager, quote).available,\n        )\n\n    def _ensure_used_funds(self, new_buy_orders, new_sell_orders, existing_orders, recently_closed_trades):\n        if not self.allow_order_funds_redispatch:\n            return\n        existing_buy_orders_count = len([\n            order for order in existing_orders if order.side is trading_enums.TradeOrderSide.BUY\n        ])\n        existing_sell_orders_count = len(existing_orders) - existing_buy_orders_count\n        updated_orders = sorted(\n            new_buy_orders + new_sell_orders + existing_orders, key=lambda t: self.get_trade_or_order_price(t)\n        )\n        if (not updated_orders) or (recently_closed_trades and self._skip_order_restore_on_recently_closed_orders):\n            # nothing to check\n            return\n        if (len(updated_orders) >= self.operational_depth\n                and self._get_max_theoretical_orders_count() > self.operational_depth):\n            # has virtual order: not supported\n            return\n        else:\n            # can more or bigger orders be created ?\n            self._ensure_full_funds_usage(updated_orders, existing_buy_orders_count, existing_sell_orders_count)\n\n    def _get_max_theoretical_orders_count(self):\n        return math.floor(\n            (self.highest_sell - self.lowest_buy - self.flat_spread + self.flat_increment) / self.flat_increment\n        ) if self.flat_increment else 0\n\n    def _ensure_full_funds_usage(self, orders, existing_buy_orders_count, existing_sell_orders_count):\n        base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        total_locked_base, total_locked_quote = self._get_locked_funds(orders)\n        max_buy_funds = trading_api.get_portfolio_currency(self.exchange_manager, quote).available + total_locked_quote\n        if self.buy_funds:\n            max_buy_funds = min(max_buy_funds, self.buy_funds)\n        max_sell_funds = trading_api.get_portfolio_currency(self.exchange_manager, base).available + total_locked_base\n        if self.sell_funds:\n            max_sell_funds = min(max_sell_funds, self.sell_funds)\n        used_buy_funds = trading_constants.ZERO\n        used_sell_funds = trading_constants.ZERO\n        total_sell_orders_value = trading_constants.ZERO\n        for order in orders:\n            order_locked_base, order_locked_quote = self._get_order_locked_funds(order)\n            buying = order.side is trading_enums.TradeOrderSide.BUY\n            if (\n                (used_buy_funds + order_locked_quote <= max_buy_funds)\n                and (buying or used_sell_funds + order_locked_base > max_sell_funds)\n            ):\n                used_buy_funds += order_locked_quote\n            else:\n                used_sell_funds += order_locked_base\n                total_sell_orders_value += order_locked_quote\n\n        # consider sell orders funds only if they are NOT drastically lower than buy orders funds\n        can_consider_sell_order_funds = total_sell_orders_value > used_buy_funds / decimal.Decimal(2)\n        # consider buy orders funds only if they are NOT drastically lower than sell orders funds\n        can_consider_buy_order_funds = used_buy_funds > total_sell_orders_value / decimal.Decimal(2)\n        if (\n            # reset if buy or sell funds are underused and sell funds are not overused\n            (\n                # has buy orders\n                existing_buy_orders_count > 0 and can_consider_buy_order_funds\n                # and buy orders are not using all funds they should\n                and used_buy_funds < max_buy_funds * self.FUNDS_INCREASE_RATIO_THRESHOLD\n                # funds locked in sell orders are lower than the theoretical max funds to sell\n                # (buy orders have not been converted into sell orders)\n                and used_sell_funds < max_sell_funds\n            )\n            or (\n                # has sell orders\n                existing_sell_orders_count > 0 and can_consider_sell_order_funds\n                # and sell orders are not using all funds they should\n                and used_sell_funds < max_sell_funds * self.FUNDS_INCREASE_RATIO_THRESHOLD\n            )\n        ):\n            self.logger.info(\n                f\"Triggering order reset: used_buy_funds={used_buy_funds}, max_buy_funds={max_buy_funds} \"\n                f\"used_sell_funds={used_sell_funds} max_sell_funds={max_sell_funds}\"\n            )\n            # bigger orders can be created\n            raise ForceResetOrdersException\n        else:\n            self.logger.debug(f\"No extra funds to dispatch\")\n\n    def get_trade_or_order_price(self, trade_or_order) -> decimal.Decimal:\n        if isinstance(trade_or_order, trading_personal_data.Order):\n            return trade_or_order.origin_price\n        if isinstance(trade_or_order, OrderData):\n            return trade_or_order.price\n        else:\n            return trade_or_order.origin_price or trade_or_order.executed_price\n\n    def _get_locked_funds(self, orders):\n        locked_base = locked_quote = trading_constants.ZERO\n        for order in orders:\n            order_locked_base, order_locked_quote = self._get_order_locked_funds(order)\n            if order.side is trading_enums.TradeOrderSide.BUY:\n                locked_quote += order_locked_quote\n            else:\n                locked_base += order_locked_base\n        return locked_base, locked_quote\n\n    def _get_order_locked_funds(self, order):\n        quantity = order.quantity if isinstance(order, OrderData) else order.origin_quantity  # don't use remaining quantity\n        price = order.price if isinstance(order, OrderData) else order.origin_price\n        return quantity, quantity * price\n\n    def _set_increment_and_spread(self, current_price, candidate_flat_increment=None):\n        origin_flat_increment = self.flat_increment\n        if self.flat_increment is None and candidate_flat_increment is not None:\n            self.flat_increment = decimal.Decimal(str(candidate_flat_increment))\n        elif self.flat_increment is None:\n            self.flat_increment = trading_personal_data.decimal_adapt_price(self.symbol_market,\n                                                                            current_price * self.increment)\n        if origin_flat_increment is not self.flat_increment:\n            self.flat_increment = trading_personal_data.decimal_adapt_price(self.symbol_market, self.flat_increment)\n        if self.flat_spread is None and self.flat_increment is not None:\n            self.flat_spread = trading_personal_data.decimal_adapt_price(\n                self.symbol_market, self.spread * self.flat_increment / self.increment\n            )\n        self.logger.debug(f\"{self.symbol} flat spread and increment initialized\")\n\n    def _get_interfering_orders_pairs(self, orders):\n        # Not a problem if allowed funds are set\n        if (self.buy_funds > 0 and self.sell_funds > 0) \\\n                or (self.buy_volume_per_order > 0 and self.sell_volume_per_order > 0):\n            return []\n        else:\n            current_base, current_quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n            interfering_pairs = set()\n            for order in orders:\n                order_symbol = order.symbol\n                if order_symbol != self.symbol:\n                    base, quote = symbol_util.parse_symbol(order_symbol).base_and_quote()\n                    if current_base == base or current_base == quote or current_quote == base or current_quote == quote:\n                        interfering_pairs.add(order_symbol)\n            return interfering_pairs\n\n    def _check_params(self):\n        if self.increment >= self.spread:\n            self.logger.error(f\"Your spread_percent parameter should always be higher than your increment_percent\"\n                              f\" parameter: average profit is spread-increment. ({self.symbol})\")\n        if self.lowest_buy >= self.highest_sell:\n            self.logger.error(f\"Your lower_bound should always be lower than your upper_bound ({self.symbol})\")\n\n    async def _handle_missed_mirror_orders_fills(self, sorted_trades, missing_orders, current_price):\n        if not self.compensate_for_missed_mirror_order or not sorted_trades or not missing_orders:\n            return\n        trades_with_missing_mirror_order_fills = self._find_missing_mirror_order_fills(sorted_trades, missing_orders)\n        if not trades_with_missing_mirror_order_fills:\n            return\n        await self._pack_and_balance_missing_orders(trades_with_missing_mirror_order_fills, current_price)\n\n    async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_order_fills, current_price):\n        base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        self.logger.info(\n            f\"Packing {len(trades_with_missing_mirror_order_fills)} missed [{self.exchange_manager.exchange_name}] \"\n            f\"mirror orders, trades {[trade.to_dict() for trade in trades_with_missing_mirror_order_fills]}\"\n        )\n        to_create_order_quantity = sum(\n            (trade.executed_quantity - trading_personal_data.get_fees_for_currency(trade.fee, base))\n            * (-1 if trade.side is trading_enums.TradeOrderSide.BUY else 1)\n            for trade in trades_with_missing_mirror_order_fills\n        )\n        self.logger.info(\n            f\"Packed {len(trades_with_missing_mirror_order_fills)} missed [{self.exchange_manager.exchange_name}] \"\n            f\"balancing quantity into: {to_create_order_quantity} {base}\"\n        )\n        if to_create_order_quantity == trading_constants.ZERO:\n            return\n        # create a market order to balance funds\n        order_type = trading_enums.TraderOrderType.SELL_MARKET if to_create_order_quantity < trading_constants.ZERO \\\n            else trading_enums.TraderOrderType.BUY_MARKET\n        target_amount = abs(to_create_order_quantity)\n        currency_available, _, market_quantity = \\\n            trading_personal_data.get_portfolio_amounts(self.exchange_manager, self.symbol, current_price)\n        limiting_amount = currency_available if order_type is trading_enums.TraderOrderType.SELL_MARKET \\\n            else market_quantity\n        if target_amount > limiting_amount:\n            # use limiting_amount if delta from order_amount is bellow allowed threshold\n            delta = target_amount - limiting_amount\n            try:\n                if delta / target_amount < self.ALLOWED_MISSED_MIRRORED_ORDERS_ADAPT_DELTA_RATIO:\n                    target_amount = limiting_amount\n                    self.logger.info(f\"Adapted balancing quantity according to available amount. Using {target_amount}\")\n            except (decimal.DivisionByZero, decimal.InvalidOperation):\n                # leave as is\n                pass\n        buying = order_type is trading_enums.TraderOrderType.BUY_MARKET\n        fees_adapted_target_amount = trading_personal_data.decimal_adapt_order_quantity_because_fees(\n            self.exchange_manager, self.symbol, order_type, target_amount,\n            current_price, trading_enums.TradeOrderSide.BUY if buying else trading_enums.TradeOrderSide.SELL,\n        )\n        if fees_adapted_target_amount != target_amount:\n            self.logger.info(\n                f\"Adapted balancing quantity to comply with exchange fees. Using {fees_adapted_target_amount} \"\n                f\"instead of {target_amount}\"\n            )\n            target_amount = fees_adapted_target_amount\n        to_create_details = trading_personal_data.decimal_check_and_adapt_order_details_if_necessary(\n            target_amount,\n            current_price,\n            self.symbol_market\n        )\n        if not to_create_details:\n            self.logger.warning(\n                f\"No enough computed funds to recreate packed missed [{self.exchange_manager.exchange_name}] \"\n                f\"mirror order balancing order on {self.symbol}: target_amount: {target_amount} is not enough \"\n                f\"for exchange minimal trading amounts rules\"\n            )\n            return\n        for order_amount, order_price in to_create_details:\n            if order_amount > limiting_amount:\n                limiting_currency = base if order_type is trading_enums.TraderOrderType.SELL_MARKET \\\n                    else quote\n                other_amount = currency_available if order_type is trading_enums.TraderOrderType.BUY_MARKET \\\n                    else market_quantity\n                other_currency = base if order_type is trading_enums.TraderOrderType.BUY_MARKET \\\n                    else quote\n                self.logger.warning(\n                    f\"No enough available funds to create missed [{self.exchange_manager.exchange_name}] mirror \"\n                    f\"order {order_type.value} balancing order on {self.symbol}. \"\n                    f\"Required {float(order_amount)} {limiting_currency}, available {float(limiting_amount)} \"\n                    f\"{limiting_currency} ({other_currency} available: {other_amount})\"\n                )\n                return\n            self.logger.info(\n                f\"{len(trades_with_missing_mirror_order_fills)} missed [{self.exchange_manager.exchange_name}] order \"\n                f\"fills on {self.symbol}, creating a {order_type.value} order of {float(order_amount)} {base} \"\n                f\"to compensate.\"\n            )\n\n            balancing_order = trading_personal_data.create_order_instance(\n                trader=self.exchange_manager.trader,\n                order_type=order_type,\n                symbol=self.symbol,\n                current_price=order_price,\n                quantity=order_amount,\n                price=order_price,\n                reduce_only=False,\n            )\n            created_order = await self.trading_mode.create_order(balancing_order)\n            # wait for order to be filled\n            await trading_personal_data.wait_for_order_fill(\n                created_order, self.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT, True\n            )\n\n    def _get_just_filled_unmirrored_missing_order_trade(self, sorted_trades, missing_order_price, missing_order_side):\n        price_increment = self.flat_spread - self.flat_increment\n        price_window = self.flat_increment / decimal.Decimal(4)\n        # each missing order should have is mirror side equivalent in recently_closed_trades\n        # when it is not the case, a fill is missing\n        now_selling = missing_order_side is trading_enums.TradeOrderSide.BUY\n        mirror_order_price = missing_order_price + price_increment if now_selling \\\n            else missing_order_price - price_increment\n        for trade in sorted_trades:\n            # use origin price if available, otherwise use executed price which is less accurate as it \n            # might be different from initial order's origin price\n            lower_window = (trade.origin_price or trade.executed_price) - price_window\n            higher_window = (trade.origin_price or trade.executed_price) + price_window\n            if lower_window < mirror_order_price < higher_window and trade.side is not missing_order_side:\n                # found mirror order fill\n                break\n            if lower_window < missing_order_price < higher_window and trade.side is missing_order_side:\n                # found missing order in trades before mirror order: this missing order has been filled but not yet \n                # replaced by a mirror order\n                return trade\n        return None\n\n    def _find_missing_mirror_order_fills(self, sorted_trades, missing_orders):\n        trades_with_missing_mirror_order_fills = []\n        \n        for missing_order_price, missing_order_side in missing_orders:\n            if trade := self._get_just_filled_unmirrored_missing_order_trade(\n                sorted_trades, missing_order_price, missing_order_side\n            ):\n                trades_with_missing_mirror_order_fills.append(trade)\n\n        if trades_with_missing_mirror_order_fills:\n\n            def _printable_trade(trade):\n                return f\"{trade.side.name} {trade.executed_quantity}@{trade.origin_price or trade.executed_price}\"\n\n            self.logger.info(\n                f\"Found {len(trades_with_missing_mirror_order_fills)} {self.symbol} missing order fills based \"\n                f\"on {len(sorted_trades)} \"\n                f\"trades. Missing fills: {[_printable_trade(t) for t in trades_with_missing_mirror_order_fills]}, \"\n                f\"trades: {[_printable_trade(t) for t in trades_with_missing_mirror_order_fills]} \"\n                f\"[{self.exchange_manager.exchange_name}]\"\n            )\n        return trades_with_missing_mirror_order_fills\n\n    async def _cancel_open_order(\n        self, order, dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ) -> tuple[bool, typing.Optional[commons_signals.SignalDependencies]]:\n        if not (order.is_cancelled() or order.is_closed()):\n            try:\n                cancelled, cancel_order_dependency = await self.trading_mode.cancel_order(order, dependencies=dependencies)\n                return cancelled, (cancel_order_dependency if cancelled else None)\n            except trading_errors.UnexpectedExchangeSideOrderStateError as err:\n                self.logger.warning(f\"Skipped order cancel: {err}, order: {order}\")\n        return False, None\n\n    async def _prepare_trailing(\n        self, sorted_orders: list, recently_closed_trades: list, \n        lowest_buy: decimal.Decimal, highest_buy: decimal.Decimal, lowest_sell: decimal.Decimal, highest_sell: decimal.Decimal, \n        current_price: decimal.Decimal, \n        dependencies: typing.Optional[commons_signals.SignalDependencies],\n    ) -> tuple[list, list, list, list, typing.Optional[commons_signals.SignalDependencies]]:\n        is_trailing_up = len([o for o in sorted_orders if o.side == trading_enums.TradeOrderSide.BUY]) > len(sorted_orders) / 2 \n        log_header = (\n            f\"[{self.exchange_manager.exchange_name}] {self.symbol} @ {current_price} \"\n            f\"{'order by order' if self.use_order_by_order_trailing else 'full grid'} \"\n            f\"trailing {'up' if is_trailing_up else 'down'} process: \"\n        )\n        if current_price <= trading_constants.ZERO:\n            self.logger.error(\n                f\"Aborting {log_header}current price is {current_price}\")\n            return [], [], [], [], None\n        if self.use_order_by_order_trailing:\n            cancelled_orders, orders, trailing_buy_orders, trailing_sell_orders, dependencies = await self._prepare_order_by_order_trailing(\n                sorted_orders, recently_closed_trades, \n                lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, is_trailing_up,\n                dependencies, log_header\n            )\n            self.is_currently_trailing = True\n            self.last_trailing_process_started_at = self.exchange_manager.exchange.get_exchange_current_time()\n            return cancelled_orders, orders, trailing_buy_orders, trailing_sell_orders, dependencies\n        return await self._prepare_full_grid_trailing(\n            sorted_orders, current_price, dependencies, log_header\n        )\n\n    async def _prepare_order_by_order_trailing(\n        self, sorted_orders: list, recently_closed_trades: list, \n        lowest_buy: decimal.Decimal, highest_buy: decimal.Decimal, \n        lowest_sell: decimal.Decimal, highest_sell: decimal.Decimal,\n        current_price: decimal.Decimal, is_trailing_up: bool,\n        dependencies: typing.Optional[commons_signals.SignalDependencies],\n        log_header: str\n    ) -> tuple[list, list, list, list, typing.Optional[commons_signals.SignalDependencies]]:\n        # 1. identify orders to cancel\n        # 1.a find and replace missing orders if any\n        replaced_buy_orders = replaced_sell_orders = []\n        try:\n            ignore_available_funds = True # trailing happens after initial funds locking, ignore & don't change initial funds\n            replaced_buy_orders, replaced_sell_orders = await self._compute_trailing_replaced_orders(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell,\n                current_price, ignore_available_funds, log_header\n            )\n            # 1.b identify orders to cancel:\n            #     cancelled = enough orders to create up to the 1st order on the other side using grid settings\n            to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price = self._get_orders_to_replace_with_updated_price_for_trailing(\n                sorted_orders, replaced_buy_orders+replaced_sell_orders, current_price\n            )\n            self.logger.info(\n                f\"{log_header} Replacing orders at prices: {[float(self.get_trade_or_order_price(o[0])) for o in (to_cancel_orders_with_trailed_prices + [to_execute_order_with_trailing_price])]} with \"\n                f\"{[float(o[1]) for o in (to_cancel_orders_with_trailed_prices + [to_execute_order_with_trailing_price])]}\"\n            )\n        except TrailingAborted as err:\n            # A normal missing order replacement should happen \n            # Can happen when all orders from a side are missing and price when back to a valid in-grid value\n            self.logger.info(f\"{log_header}trailing aborted: {err}. Replacing orders with: {replaced_buy_orders=} {replaced_sell_orders=}\")\n            return [], [], replaced_buy_orders, replaced_sell_orders, None\n        except NoOrdersToTrail as err:\n            # happens when all orders are filled at once: use the \"new\" state to recreate the grid, should be rare\n            self.logger.warning(\n                f\"{log_header}no order to trail from, using full grid trailing to balance funds before recreating the grid: {err}\"\n            )\n            return await self._prepare_full_grid_trailing(sorted_orders, current_price, dependencies, log_header)\n        except ValueError as err:\n            self.logger.error(f\"{log_header}error when identifying orders to cancel: {err}\")\n            return [], [], [], [], None\n        # 2. cancel orders to be replaced with updated prices\n        cancelled_replaced_orders, cancelled_orders, convert_dependencies = await self._cancel_replaced_orders(\n            [order for order, _ in to_cancel_orders_with_trailed_prices + [to_execute_order_with_trailing_price]],\n            dependencies\n        )\n        # 3. execute extrema order amount as market order to convert funds\n        to_convert_order = to_execute_order_with_trailing_price[0]\n        orders = await self._convert_order_funds(\n            to_convert_order, current_price, convert_dependencies, log_header\n        )\n        orders_dependencies = signals.get_orders_dependencies(orders)\n        # 4. compute trailing orders\n        trailing_buy_orders, trailing_sell_orders = self._get_updated_trailing_orders(\n            replaced_buy_orders, replaced_sell_orders, cancelled_replaced_orders, \n            to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price, current_price,\n            is_trailing_up\n        )\n        self.logger.info(f\"{log_header}creating {len(trailing_buy_orders)} buy orders and {len(trailing_sell_orders)} sell orders: {trailing_buy_orders=} {trailing_sell_orders=}\")\n        return cancelled_orders, orders, trailing_buy_orders, trailing_sell_orders, (orders_dependencies or convert_dependencies or None)\n\n    async def _cancel_replaced_orders(\n        self, replaced_orders: list[typing.Union[OrderData, trading_personal_data.Order]], dependencies\n    ) -> tuple[list[OrderData], list[trading_personal_data.Order], commons_signals.SignalDependencies]:\n        cancelled_orders = []\n        cancelled_replaced_orders = []\n        new_dependencies = commons_signals.SignalDependencies()\n        for order in replaced_orders:\n            if isinstance(order, OrderData):\n                cancelled_replaced_orders.append(order)\n            else:\n                cancelled, cancel_order_dependency = await self._cancel_open_order(order, dependencies)\n                if cancelled:\n                    cancelled_orders.append(order)\n                    if cancel_order_dependency:\n                        new_dependencies.extend(cancel_order_dependency)\n\n        return cancelled_replaced_orders, cancelled_orders, new_dependencies\n\n    async def _compute_trailing_replaced_orders(\n        self, sorted_orders, recently_closed_trades, \n        lowest_buy, highest_buy, lowest_sell, highest_sell, \n        current_price, ignore_available_funds, log_header\n    ) -> tuple[list[OrderData], list[OrderData]]:\n        missing_orders, state, _ = self._analyse_current_orders_situation(\n            sorted_orders, recently_closed_trades, lowest_buy, highest_sell, current_price\n        )\n        if state == self.NEW and not sorted_orders:\n            raise NoOrdersToTrail(f\"no open order to trail, nothing to replace\")\n        if state != self.FILL:\n            raise ValueError(f\"unhandled state: {state} (expected: self.FILL: {self.FILL})\")\n        replaced_buy_orders = replaced_sell_orders = []\n        if missing_orders:\n            self.logger.info(\n                f\"{log_header}found {len(missing_orders)} missing orders: preparing orders before \"\n                f\"order by order trailing process {missing_orders}\"\n            )\n            await self._handle_missed_mirror_orders_fills(recently_closed_trades, missing_orders, current_price)\n            replaced_buy_orders = self._create_orders(\n                lowest_buy, highest_buy, trading_enums.TradeOrderSide.BUY, sorted_orders,\n                current_price, missing_orders, state, self.buy_funds, ignore_available_funds, recently_closed_trades\n            )\n            replaced_sell_orders = self._create_orders(\n                lowest_sell, highest_sell, trading_enums.TradeOrderSide.SELL, sorted_orders,\n                current_price, missing_orders, state, self.sell_funds, ignore_available_funds, recently_closed_trades\n            )\n        return replaced_buy_orders, replaced_sell_orders\n\n    async def _convert_order_funds(\n        self, to_convert_order, current_price, convert_dependencies, log_header\n    ) -> list[trading_personal_data.Order]:\n        base, quote = symbol_util.parse_symbol(to_convert_order.symbol).base_and_quote()\n        base_amount_to_convert = to_convert_order.quantity if isinstance(to_convert_order, OrderData) \\\n            else to_convert_order.get_remaining_quantity()\n        if to_convert_order.side is trading_enums.TradeOrderSide.BUY:\n            # replace buy order by a sell order => convert quote to base\n            to_sell = quote\n            to_buy = base\n            amount_to_convert = base_amount_to_convert * self.get_trade_or_order_price(to_convert_order)\n        else:\n            # replace sell order by a buy order => convert base to quote\n            to_sell = base\n            to_buy = quote\n            amount_to_convert = base_amount_to_convert\n        self.logger.info(f\"{log_header}selling {amount_to_convert} {base} worth of {to_sell} to buy {to_buy}\")\n        # need portfolio available to be up-to-date with cancelled orders\n        orders = await trading_modes.convert_asset_to_target_asset(\n            self.trading_mode, to_sell, to_buy, {\n                self.symbol: {\n                    trading_enums.ExchangeConstantsTickersColumns.CLOSE.value: current_price,\n                }\n            }, asset_amount=amount_to_convert,\n            dependencies=convert_dependencies\n        )\n        orders = [order for order in orders if order is not None]\n        if orders:\n            await asyncio.gather(*[\n                trading_personal_data.wait_for_order_fill(\n                    order, self.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT, True\n                ) for order in orders\n            ])\n        return orders\n\n    def _get_updated_trailing_orders(\n        self, replaced_buy_orders, replaced_sell_orders, cancelled_replaced_orders, \n        to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price, current_price,\n        is_trailing_up\n    ) -> tuple[list[OrderData], list[OrderData]]:\n        to_convert_order = to_execute_order_with_trailing_price[0]\n        trailing_buy_orders = [\n            buy_order for buy_order in replaced_buy_orders if buy_order not in cancelled_replaced_orders\n        ]\n        trailing_sell_orders = [\n            sell_order for sell_order in replaced_sell_orders if sell_order not in cancelled_replaced_orders\n        ]\n        # add orders with price covering up to the current price\n        for cancelled_order, trailed_price in (to_cancel_orders_with_trailed_prices + [to_execute_order_with_trailing_price]):\n            trailed_order_side = (\n                trading_enums.TradeOrderSide.BUY if trailed_price <= current_price else trading_enums.TradeOrderSide.SELL\n            )\n            if cancelled_order is to_convert_order:\n                # force the order side to be the opposite of the trailing direction and make sure this order \n                # gets created at the side, even if it's at the current price\n                trailed_order_side = trading_enums.TradeOrderSide.SELL if is_trailing_up else trading_enums.TradeOrderSide.BUY\n                ideal_base_quantity = to_convert_order.total_cost / trailed_price \n                parsed_symbol = symbol_util.parse_symbol(to_convert_order.symbol)\n                other_side_currency = parsed_symbol.quote if trailed_order_side is trading_enums.TradeOrderSide.BUY else parsed_symbol.base\n                available_amount = trading_api.get_portfolio_currency(self.exchange_manager, other_side_currency).available\n                available_amount_in_base = available_amount if other_side_currency == parsed_symbol.base else available_amount / trailed_price\n                if available_amount_in_base < ideal_base_quantity:\n                    trailing_order_quantity = available_amount_in_base\n                    self.logger.warning(\n                        f\"Not enough available funds to create a full {ideal_base_quantity} {parsed_symbol.base} {to_convert_order.symbol} {trailed_order_side.name} trailing \"\n                        f\"order: available: {available_amount} {other_side_currency} < {ideal_base_quantity} \"\n                        f\"(={available_amount_in_base} {parsed_symbol.base}). Using {trailing_order_quantity} instead.\"\n                    )\n                else:\n                    trailing_order_quantity = ideal_base_quantity\n            else:\n                initial_quantity = cancelled_order.quantity if isinstance(cancelled_order, OrderData) \\\n                    else cancelled_order.get_remaining_quantity()\n                if cancelled_order.side is trading_enums.TradeOrderSide.BUY:\n                    # trailed buy orders inherit the total cost of the orders they are replacing\n                    initial_price = self.get_trade_or_order_price(cancelled_order)\n                    trailing_order_quantity = initial_quantity * initial_price / trailed_price\n                else:\n                    # trailed sell orders can inherit the quantity of the orders they are replacing\n                    trailing_order_quantity = initial_quantity\n            order = OrderData(\n                trailed_order_side, trailing_order_quantity, trailed_price, self.symbol, False\n            )\n            if trailed_order_side is trading_enums.TradeOrderSide.BUY:\n                trailing_buy_orders.append(order)\n            else:\n                trailing_sell_orders.append(order)\n        return trailing_buy_orders, trailing_sell_orders\n\n    def _get_orders_to_replace_with_updated_price_for_trailing(\n        self, sorted_orders: list[trading_personal_data.Order], replaced_orders: list[OrderData], current_price: decimal.Decimal\n    ) -> tuple[\n        list[tuple[typing.Union[trading_personal_data.Order, OrderData], decimal.Decimal]], \n        tuple[trading_personal_data.Order, decimal.Decimal]\n    ]:\n        if not sorted_orders:\n            raise ValueError(f\"No input sorted orders, trailing can't happen on {self.symbol}\")\n        confirmed_sorted_grid_prices = sorted([\n            replaced_order.price for replaced_order in replaced_orders\n        ] + [\n            order.origin_price for order in (sorted_orders)\n        ])\n        is_trailing_up = current_price > confirmed_sorted_grid_prices[-1]\n        if not is_trailing_up and current_price >= confirmed_sorted_grid_prices[0]:\n            raise TrailingAborted(\n                f\"Current price is not beyond grid boundaries: {current_price}, \"\n                f\"grid min: {confirmed_sorted_grid_prices[0]}, grid max: {confirmed_sorted_grid_prices[-1]}\"\n            )\n        orders_to_replace_with_trailed_price: list[\n            tuple[typing.Union[trading_personal_data.Order, OrderData], decimal.Decimal]\n        ] = []\n        if not (self.flat_increment and self.flat_spread):\n            raise ValueError(\n                f\"Flat increment and flat spread mush be set {self.flat_increment=} {self.flat_spread=}\"\n            )\n        if is_trailing_up:\n            # trailing up: free enough funds to create orders up to the current price, including 1 sell order above the current price\n            extrema_order_price = confirmed_sorted_grid_prices[-1]\n            if extrema_order_price + self.flat_spread > current_price:\n                # no order to create, only the other side order to handle\n                order_count_to_create = 0\n            else:\n                order_count_to_create = math.ceil((current_price - self.flat_spread - extrema_order_price) / self.flat_increment)\n            other_side_order_price = extrema_order_price + (self.flat_increment * order_count_to_create) + self.flat_spread\n        else:\n            # trailing down: free enough funds to create orders down to the current price, including 1 buy order below the current price\n            extrema_order_price = confirmed_sorted_grid_prices[0]\n            if extrema_order_price - self.flat_spread < current_price:\n                # no order to create, only the other side order to handle\n                order_count_to_create = 0\n            else:\n                order_count_to_create = math.ceil((extrema_order_price - self.flat_spread - current_price) / self.flat_increment)\n            other_side_order_price = extrema_order_price - (self.flat_increment * order_count_to_create) - self.flat_spread\n\n        order_by_price: dict[decimal.Decimal, typing.Union[trading_personal_data.Order, OrderData]] = {\n            self.get_trade_or_order_price(order): order\n            for order in replaced_orders + sorted_orders\n        }\n        # order_to_replace_by_other_side_order should be an open order, not a replaced order\n        order_to_replace_by_other_side_order_price = sorted_orders[0].origin_price if is_trailing_up else sorted_orders[-1].origin_price\n        order_to_replace_by_other_side_order = order_by_price[order_to_replace_by_other_side_order_price]\n        if not isinstance(order_to_replace_by_other_side_order, trading_personal_data.Order):\n            # should never happen\n            raise ValueError(f\"Order to replace by other side order is not an open order: {order_to_replace_by_other_side_order}\")\n        confirmed_prices = [\n            price for price in confirmed_sorted_grid_prices if price != order_to_replace_by_other_side_order_price\n        ]\n        # 1 trailing price per confirmed price (don't create more than the number of confirmed prices in case price is way off)\n        trailing_order_prices = [\n            extrema_order_price + (self.flat_increment * (i + 1) * (1 if is_trailing_up else -1))\n            for i in range(int(order_count_to_create))\n        ][-len(confirmed_prices):]\n        remaining_order_prices = collections.deque(sorted(\n            confirmed_prices, key=lambda price: price if is_trailing_up else -price\n        ))\n        self.logger.info(f\"trailing_order_prices: {trailing_order_prices} {confirmed_prices=}\")\n        for trailing_order_price in trailing_order_prices:\n            # associate each new order price to an existing order\n            order_to_replace_price = remaining_order_prices.popleft()\n            order_to_replace  = order_by_price[order_to_replace_price]\n            orders_to_replace_with_trailed_price.append((order_to_replace, trailing_order_price))\n\n        return orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price)\n\n\n    async def _prepare_full_grid_trailing(\n        self, open_orders: list, current_price: decimal.Decimal, \n        dependencies: typing.Optional[commons_signals.SignalDependencies],\n        log_header: str\n    ) -> tuple[list, list, list, list, typing.Optional[commons_signals.SignalDependencies], bool]:\n        # 1. cancel all open orders\n        convert_dependencies = commons_signals.SignalDependencies()\n        try:\n            cancelled_orders = []\n            self.logger.info(f\"{log_header}cancelling {len(open_orders)} open orders on {self.symbol}\")\n            for order in open_orders:\n                cancelled, cancel_order_dependency = await self._cancel_open_order(order, dependencies)\n                if cancelled:\n                    cancelled_orders.append(order)\n                    if cancel_order_dependency:\n                        convert_dependencies.extend(cancel_order_dependency)\n        except Exception as err:\n            self.logger.exception(err, True, f\"Error in {log_header} cancel orders step: {err}\")\n            cancelled_orders = []\n\n        # 2. if necessary, convert a part of the funds to be able to create buy and sell orders\n        orders = []\n        try:\n            parsed_symbol = symbol_util.parse_symbol(self.symbol)\n            available_base_amount = trading_api.get_portfolio_currency(self.exchange_manager, parsed_symbol.base).available\n            available_quote_amount = trading_api.get_portfolio_currency(self.exchange_manager, parsed_symbol.quote).available\n            usable_amount_in_quote = available_quote_amount + (available_base_amount * current_price)\n            config_max_amount = self.buy_funds + (self.sell_funds * current_price)\n            if config_max_amount > trading_constants.ZERO:\n                usable_amount_in_quote = min(usable_amount_in_quote, config_max_amount)\n            # amount = the total amount (in base) to put into the grid at the current price\n            usable_amount_in_base = usable_amount_in_quote / current_price\n\n            target_base = usable_amount_in_base / decimal.Decimal(2)\n            target_quote = usable_amount_in_quote / decimal.Decimal(2)\n\n            amount = trading_constants.ZERO\n            to_sell = to_buy = None\n            if available_base_amount < target_base:\n                # buy order\n                to_buy = parsed_symbol.base\n                to_sell = parsed_symbol.quote\n                amount = (target_base - available_base_amount) * current_price\n            if available_quote_amount < target_quote:\n                if amount != trading_constants.ZERO:\n                    # can't buy with currencies, this should never happen: log error\n                    self.logger.error(f\"{log_header}can't buy and sell {parsed_symbol} at the same time.\")\n                else:\n                    # sell order\n                    to_buy = parsed_symbol.quote\n                    to_sell = parsed_symbol.base\n                    amount = (target_quote - available_quote_amount) / current_price\n\n            if amount > trading_constants.ZERO:\n                self.logger.info(f\"{log_header}selling {amount} {to_sell} to buy {to_buy}\")\n                # need portfolio available to be up-to-date with cancelled orders\n                orders = await trading_modes.convert_asset_to_target_asset(\n                    self.trading_mode, to_sell, to_buy, {\n                        self.symbol: {\n                            trading_enums.ExchangeConstantsTickersColumns.CLOSE.value: current_price,\n                        }\n                    }, asset_amount=amount,\n                    dependencies=convert_dependencies\n                )\n                if orders:\n                    await asyncio.gather(*[\n                        trading_personal_data.wait_for_order_fill(\n                            order, self.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT, True\n                        ) for order in orders\n                    ])\n            else:\n                self.logger.info(f\"{log_header}nothing to buy or sell. Current funds are enough\")\n        except Exception as err:\n            self.logger.exception(\n                err, True, f\"Error in {log_header}convert into target step: {err}\"\n            )\n\n        # 3. reset available funds (free funds from cancelled orders)\n        self._reset_available_funds()\n\n        self.logger.info(\n            f\"Completed {log_header} {len(cancelled_orders)} cancelled orders, {len(orders)} \"\n            f\"created conversion orders\"\n        )\n        orders_dependencies = signals.get_orders_dependencies(orders)\n        self.is_currently_trailing = True\n        self.last_trailing_process_started_at = self.exchange_manager.exchange.get_exchange_current_time()\n        return cancelled_orders, orders, [], [], (orders_dependencies or convert_dependencies or None)\n\n    def _analyse_current_orders_situation(self, sorted_orders, recently_closed_trades, lower_bound, higher_bound, current_price):\n        if not sorted_orders:\n            return None, self.NEW, None\n        # check if orders are staggered orders\n        return self._bootstrap_parameters(sorted_orders, recently_closed_trades, lower_bound, higher_bound, current_price)\n\n    def _create_orders(self, lower_bound, upper_bound, side, sorted_orders,\n                       current_price, missing_orders, state, allowed_funds, ignore_available_funds, recent_trades) -> list[OrderData]:\n\n        if lower_bound == upper_bound:\n            self.logger.info(\n                f\"No {side.name} orders to create for {self.symbol} lower bound = upper bound = {upper_bound}\"\n            )\n            return []\n        if lower_bound > upper_bound:\n            self.logger.warning(\n                f\"No {side.name} orders to create for {self.symbol}: \"\n                f\"Your configured increment or spread value is likely too large for the current price. \"\n                f\"Current price: {current_price}, increment: {self.flat_increment}, spread: {self.flat_spread}. \"\n                f\"Current price beyond boundaries: \"\n                f\"computed lower bound: {lower_bound}, computed upper bound: {upper_bound}. \"\n                f\"Lower bound should be inferior to upper bound.\"\n            )\n            return []\n\n        selling = side == trading_enums.TradeOrderSide.SELL\n\n        currency, market = symbol_util.parse_symbol(self.symbol).base_and_quote()\n        order_limiting_currency = currency if selling else market\n\n        order_limiting_currency_amount = trading_api.get_portfolio_currency(self.exchange_manager, order_limiting_currency).available\n        if state == self.NEW:\n            # create staggered orders\n            return self._create_new_orders_bundle(\n                lower_bound, upper_bound, side, current_price, allowed_funds, ignore_available_funds, selling,\n                order_limiting_currency, order_limiting_currency_amount\n            )\n        if state == self.FILL:\n            # complete missing orders\n            orders = self._fill_missing_orders(\n                lower_bound, upper_bound, side, sorted_orders, current_price, missing_orders, selling,\n                order_limiting_currency, order_limiting_currency_amount, currency, recent_trades\n            )\n            return orders\n        if state == self.ERROR:\n            self.logger.error(f\"Impossible to create {self.ORDERS_DESC} orders for {self.symbol} when incompatible \"\n                              f\"order are already in place. Cancel these orders of you want to use this trading mode.\")\n        return []\n\n    def _create_new_orders_bundle(\n        self, lower_bound, upper_bound, side, current_price, allowed_funds, ignore_available_funds, selling,\n        order_limiting_currency, order_limiting_currency_amount\n    ) -> list[OrderData]:\n        orders = []\n        funds_to_use = self._get_maximum_traded_funds(allowed_funds,\n                                                      order_limiting_currency_amount,\n                                                      order_limiting_currency,\n                                                      selling,\n                                                      ignore_available_funds)\n        if funds_to_use == 0:\n            return []\n        starting_bound = lower_bound * (1 + self.spread / 2) if selling else upper_bound * (1 - self.spread / 2)\n        self.flat_spread = trading_personal_data.decimal_adapt_price(self.symbol_market,\n                                                                     current_price * self.spread)\n        self._create_new_orders(orders, current_price, selling, lower_bound, upper_bound,\n                                funds_to_use, order_limiting_currency, starting_bound, side,\n                                True, self.mode, order_limiting_currency_amount)\n        return orders\n\n    def _fill_missing_orders(\n        self, lower_bound, upper_bound, side, sorted_orders, current_price, missing_orders, selling,\n        order_limiting_currency, order_limiting_currency_amount, currency, recent_trades\n    ):\n        orders = []\n        if missing_orders and [o for o in missing_orders if o[1] is side]:\n            max_quant_per_order = order_limiting_currency_amount / len([o for o in missing_orders if o[1] is side])\n            missing_orders_around_spread = []\n            for missing_order_price, missing_order_side in missing_orders:\n                if missing_order_side == side:\n                    previous_o = None\n                    following_o = None\n                    for o in sorted_orders:\n                        if previous_o is None:\n                            previous_o = o\n                        elif o.origin_price > missing_order_price:\n                            following_o = o\n                            break\n                        else:\n                            previous_o = o\n                    if following_o is None or previous_o.side == following_o.side:\n                        decimal_missing_order_price = decimal.Decimal(str(missing_order_price))\n                        # missing order between similar orders\n                        quantity = self._get_surrounded_missing_order_quantity(\n                            previous_o, following_o, max_quant_per_order, decimal_missing_order_price, recent_trades,\n                            current_price, sorted_orders, side\n                        )\n                        orders.append(OrderData(missing_order_side, quantity,\n                                                decimal_missing_order_price, self.symbol, False))\n                        self.logger.debug(f\"Creating missing orders not around spread: {orders[-1]} \"\n                                          f\"for {self.symbol}\")\n                    else:\n                        missing_orders_around_spread.append((missing_order_price, missing_order_side))\n\n            if missing_orders_around_spread:\n                # missing order next to spread\n                starting_bound = upper_bound if selling else lower_bound\n                increment_window = self.flat_increment / 2\n                order_limiting_currency_available_amount = trading_api.get_portfolio_currency(\n                    self.exchange_manager, order_limiting_currency\n                ).available\n                decimal_order_limiting_currency_available_amount = decimal.Decimal(\n                    str(order_limiting_currency_available_amount))\n                portfolio_total = trading_api.get_portfolio_currency(self.exchange_manager,\n                                                                     order_limiting_currency).total\n                order_limiting_currency_amount = portfolio_total\n                if order_limiting_currency_available_amount:\n                    orders_count, average_order_quantity = \\\n                        self._get_order_count_and_average_quantity(\n                            current_price, selling, lower_bound, upper_bound, portfolio_total, currency, self.mode\n                        )\n\n                    for missing_order_price, missing_order_side in missing_orders_around_spread:\n                        added_missing_order = False\n                        limiting_amount_from_this_order = order_limiting_currency_amount\n                        price = starting_bound - self.flat_increment if selling else starting_bound + self.flat_increment\n                        found_order = False\n                        exceeded_price = False\n                        i = 0\n                        max_orders_count = max(orders_count, self.operational_depth)\n                        while not (\n                            found_order or exceeded_price or\n                            limiting_amount_from_this_order < trading_constants.ZERO or\n                            i >= max_orders_count\n                        ):\n                            if price != 0:\n                                order_quantity = self._get_spread_missing_order_quantity(\n                                    average_order_quantity, side, i, orders_count, price, selling,\n                                    limiting_amount_from_this_order,\n                                    decimal_order_limiting_currency_available_amount, recent_trades, sorted_orders,\n                                    current_price\n                                )\n                                if price is not None and limiting_amount_from_this_order > 0 and \\\n                                        price - increment_window <= missing_order_price <= price + increment_window:\n                                    found_order = True\n                                    if order_quantity is not None:\n                                        orders.append(OrderData(side, decimal.Decimal(str(order_quantity)),\n                                                                decimal.Decimal(str(missing_order_price)), self.symbol,\n                                                                False))\n                                        added_missing_order = True\n                                        self.logger.debug(f\"Creating missing order around spread {orders[-1]} \"\n                                                          f\"for {self.symbol}\")\n                                if order_quantity is not None:\n                                    used_amount = order_quantity if selling else order_quantity * price\n                                    limiting_amount_from_this_order -= used_amount\n                            price = price - self.flat_increment if selling else price + self.flat_increment\n                            if (\n                                selling and price < (missing_order_price - self.flat_increment)\n                            ) or (\n                                (not selling) and price > missing_order_price + self.flat_increment\n                            ):\n                                exceeded_price = True\n                            i += 1\n                        if not added_missing_order:\n                            self.logger.warning(\n                                f\"Missing order not restored: price {missing_order_price} side: {missing_order_side}\"\n                            )\n        return orders\n\n    def _get_surrounded_missing_order_quantity(\n        self, previous_order, following_order, max_quant_per_order, order_price, recent_trades,\n            current_price, sorted_orders, side\n    ):\n        selling = side == trading_enums.TradeOrderSide.SELL\n        if sorted_orders:\n            if quantity := self._get_quantity_from_existing_orders(\n                order_price, sorted_orders, selling\n            ):\n                return quantity\n        quantity_from_trades = self._get_quantity_from_recent_trades(\n            order_price, max_quant_per_order, recent_trades, current_price, selling\n        )\n        return quantity_from_trades or \\\n            decimal.Decimal(str(\n                min(\n                    data_util.mean([previous_order.origin_quantity, following_order.origin_quantity])\n                    if following_order else previous_order.origin_quantity,\n                    (max_quant_per_order if selling else max_quant_per_order / order_price)\n                )\n            ))\n\n    def _get_spread_missing_order_quantity(\n        self, average_order_quantity, side, i, orders_count, price, selling, limiting_amount_from_this_order,\n        order_limiting_currency_available_amount, recent_trades, sorted_orders,\n        current_price\n    ):\n        quantity = None\n        if sorted_orders:\n            quantity = self._get_quantity_from_existing_orders(\n                price, sorted_orders, selling\n            )\n            if quantity:\n                # quantity is from currently open orders: use it as is\n                return quantity\n        # quantity is not in open orders: infer it\n        if not quantity:\n            quantity = self._get_quantity_from_recent_trades(\n                price, limiting_amount_from_this_order, recent_trades, current_price, selling\n            )\n        if not quantity:\n            try:\n                quantity = self._get_quantity_from_iteration(\n                    average_order_quantity, self.mode, side, i, orders_count, price, price\n                )\n            except trading_errors.NotSupported:\n                quantity = self._get_quantity_from_existing_boundary_orders(\n                    price, sorted_orders, selling\n                )\n                if quantity:\n                    self.logger.info(\n                        f\"Using boundary orders to compute restored order quantity for {'sell' if selling else 'buy'} \"\n                        f\"order at {price}: no equivalent order for in recent trades (recent trades: \"\n                        f\"{[str(t) for t in recent_trades]}).\"\n                    )\n                else:\n                    self.logger.error(\n                        f\"Error when computing restored order quantity for {'sell' if selling else 'buy'} order at \"\n                        f\"price: {price}: recent trades or active orders are required.\"\n                    )\n                    return None\n        if quantity is None:\n            return None\n        # always ensure ideal quantity is available\n        limiting_currency_quantity = quantity\n        limiting_cost = limiting_currency_quantity if selling else limiting_currency_quantity * price\n        if limiting_cost > limiting_amount_from_this_order or \\\n                limiting_cost > order_limiting_currency_available_amount:\n            limiting_cost = min(\n                limiting_amount_from_this_order,\n                order_limiting_currency_available_amount\n            )\n        try:\n            return limiting_cost if selling else limiting_cost / price\n        except decimal.DecimalException as err:\n            self.logger.exception(err, True, f\"Error when computing missing order quantity: {err}\")\n            return limiting_currency_quantity\n\n    def _get_quantity_from_existing_orders(self, price, sorted_orders, selling):\n        increment_window = self.flat_increment / 4\n        price_window_lower_bound = price - increment_window\n        price_window_higher_bound = price + increment_window\n        for order in sorted_orders:\n            if price_window_lower_bound <= order.origin_price <= price_window_higher_bound and (\n                order.side is (trading_enums.TradeOrderSide.SELL if selling else trading_enums.TradeOrderSide.BUY)\n            ):\n                return order.origin_quantity\n        return None\n\n    def _get_quantity_from_existing_boundary_orders(self, price, sorted_orders, selling):\n        # Should be the last attempt: compute price from existing orders using cost\n        # of the 1st order on target side and compute linear quantity. Use boundary order as it has the most chances\n        # to remain according to the initial orders costs (compared to an average that could contain results of trades\n        # from the order side, which cost might not be balanced with the current order side)\n        example_order = sorted_orders[-1] if selling else sorted_orders[0]\n        target_side = trading_enums.TradeOrderSide.SELL if selling else trading_enums.TradeOrderSide.BUY\n        if example_order.side is not target_side:\n            # an order from the same side is required\n            return None\n        target_cost = example_order.total_cost\n        # use linear equivalent of the target cost\n        return target_cost / price\n\n    def _get_quantity_from_recent_trades(self, price, max_quantity, recent_trades, current_price, selling):\n        if not self._use_recent_trades_for_order_restore or not recent_trades:\n            return None\n        # try to find accurate quantity from the available recent trades\n        trade = self._get_associated_trade(price, recent_trades, selling)\n        if trade is None:\n            return None\n        now_selling = trade.side == trading_enums.TradeOrderSide.BUY\n        return self._compute_mirror_order_volume(\n            now_selling, (trade.origin_price or trade.executed_price), price, trade.executed_quantity, trade.fee\n        )\n\n    def _get_associated_trade(self, price, trades, selling):\n        increment_window = self.flat_increment / 4\n        price_window_lower_bound = price - increment_window\n        price_window_higher_bound = price + increment_window\n        for trade in trades:\n            is_sell_trade = trade.side == trading_enums.TradeOrderSide.SELL\n            trade_price = trade.origin_price or trade.executed_price\n            if is_sell_trade == selling:\n                # same side\n                if price_window_lower_bound <= trade_price <= price_window_higher_bound:\n                    # found the exact same trade\n                    return trade\n            else:\n                # different side: use spread to compute mirror order price\n                price_increment = self.flat_spread - self.flat_increment\n                mirror_order_price = (trade_price - price_increment) \\\n                    if is_sell_trade else (trade_price + price_increment)\n                if price_window_lower_bound <= mirror_order_price <= price_window_higher_bound:\n                    # found mirror trade\n                    return trade\n        return None\n\n    def _get_maximum_traded_funds(self, allowed_funds, total_available_funds, currency, selling, ignore_available_funds):\n        to_trade_funds = total_available_funds\n        if allowed_funds > 0:\n            if total_available_funds < allowed_funds:\n                self.logger.warning(\n                    f\"Impossible to create every {self.ORDERS_DESC} orders for {self.symbol} using the total \"\n                    f\"{'sell' if selling else 'buy'} funds configuration ({allowed_funds}): not enough \"\n                    f\"available {currency} funds ({total_available_funds}). Trying to use available funds only.\")\n                to_trade_funds = total_available_funds\n            else:\n                to_trade_funds = allowed_funds\n        if not ignore_available_funds and self._is_initially_available_funds_set(currency):\n            # check if enough funds are available\n            unlocked_funds = self._get_available_funds(currency)\n            if to_trade_funds > unlocked_funds:\n                if unlocked_funds <= 0:\n                    self.logger.error(f\"Impossible to create {self.ORDERS_DESC} orders for {self.symbol}: {currency} \"\n                                      f\"funds are already locked for other trading pairs.\")\n                    return 0\n                self.logger.warning(f\"Impossible to create {self.ORDERS_DESC} orders for {self.symbol} using the \"\n                                    f\"total funds ({allowed_funds}): {currency} funds are already locked for other \"\n                                    f\"trading pairs. Trying to use remaining funds only.\")\n                to_trade_funds = unlocked_funds\n        return to_trade_funds\n\n    def _create_new_orders(self, orders, current_price, selling, lower_bound, upper_bound,\n                           order_limiting_currency_amount, order_limiting_currency, starting_bound, side,\n                           virtual_orders, mode, total_available_funds):\n        orders_count, average_order_quantity = \\\n            self._get_order_count_and_average_quantity(current_price, selling, lower_bound,\n                                                       upper_bound, order_limiting_currency_amount,\n                                                       order_limiting_currency, mode)\n        # orders closest to the current price are added first\n        for i in range(orders_count):\n            price = self._get_price_from_iteration(starting_bound, selling, i)\n            if price is not None:\n                quantity = self._get_quantity_from_iteration(average_order_quantity, mode,\n                                                             side, i, orders_count, price, starting_bound)\n                if quantity is not None:\n                    orders.append(OrderData(side, quantity, price, self.symbol, virtual_orders))\n        if not orders:\n            message = \"change change the strategy settings to make less but bigger orders.\" \\\n                if self._use_variable_orders_volume(side) else \\\n                f\"reduce {'buy' if side is trading_enums.TradeOrderSide.BUY else 'sell'} the orders volume.\"\n            # Todo: send it as visible notification to the user instead of warning/error\n            self.logger.warning(\n                f\"Not enough {order_limiting_currency} to create {side.name} orders. \"\n                f\"For the strategy to work better, add {order_limiting_currency} funds or \"\n                f\"{message}\"\n            )\n        else:\n            # register the locked orders funds\n            if not self._is_initially_available_funds_set(order_limiting_currency):\n                self._set_initially_available_funds(order_limiting_currency, total_available_funds)\n\n    def _bootstrap_parameters(self, sorted_orders, recently_closed_trades, lower_bound, higher_bound, current_price):\n        # no decimal.Decimal computation here\n        mode = self.mode or None\n        spread = None\n        increment = self.flat_increment or None\n        bigger_buys_closer_to_center = None\n        first_sell = None\n        ratio = None\n        state = self.FILL\n\n        missing_orders = []\n\n        previous_order = None\n\n        only_sell = False\n        only_buy = False\n        if sorted_orders:\n            if sorted_orders[0].side == trading_enums.TradeOrderSide.SELL:\n                # only sell orders\n                (self.logger.info if self.enable_trailing_down else self.logger.warning)(\n                    f\"Only sell orders are online for {self.symbol}, \"\n                    f\"{'checking trailing' if self.enable_trailing_down else 'now waiting for the price to go up to create new buy orders'}.\"\n                )\n                first_sell = sorted_orders[0]\n                only_sell = True\n            if sorted_orders[-1].side == trading_enums.TradeOrderSide.BUY:\n                # only buy orders\n                (self.logger.info if self.enable_trailing_up else self.logger.warning)(\n                    f\"Only buy orders are online ({len(sorted_orders)} orders) for {self.symbol}, \"\n                    f\"{'checking trailing' if self.enable_trailing_up else 'now waiting for the price to go down to create new sell orders'}.\"\n                )\n                only_buy = True\n            for order in sorted_orders:\n                if order.symbol != self.symbol:\n                    self.logger.warning(f\"Error when analyzing orders for {self.symbol}: order.symbol != self.symbol.\")\n                    return None, self.ERROR, None\n                spread_point = False\n                if previous_order is None:\n                    previous_order = order\n                else:\n                    if previous_order.side != order.side:\n                        # changing order side: reached spread point\n                        if spread is None:\n                            if lower_bound < self.current_price < higher_bound:\n                                spread_point = True\n                                delta_spread = order.origin_price - previous_order.origin_price\n\n                                if increment is None:\n                                    self.logger.warning(f\"Error when analyzing orders for {self.symbol}: increment \"\n                                                        f\"is None.\")\n                                    return None, self.ERROR, None\n                                else:\n                                    inferred_spread = self.flat_spread or self.spread * increment / self.increment\n                                    missing_orders_count = (delta_spread - inferred_spread) / increment\n                                    # should be 0 when no order is missing\n                                    if float(missing_orders_count) > 0.5:\n                                        # missing orders around spread point: symmetrical orders were not created when\n                                        # orders were filled => re-create them\n                                        next_missing_order_price = previous_order.origin_price + increment\n                                        spread_lower_boundary = order.origin_price - inferred_spread\n\n                                        # re-create buy orders starting from the closest buy up to spread\n                                        while next_missing_order_price < self.current_price and \\\n                                                next_missing_order_price <= spread_lower_boundary:\n                                            # missing buy order\n                                            if next_missing_order_price + increment > spread_lower_boundary:\n                                                # This potential missing buy is the last before spread. Before considering it missing,\n                                                # make sure that the missing order is not on the selling side of the spread (and \n                                                # therefore the missing order should be a sell)\n                                                if recently_closed_trades and self._get_just_filled_unmirrored_missing_order_trade(\n                                                    recently_closed_trades, next_missing_order_price, trading_enums.TradeOrderSide.BUY\n                                                ):\n                                                    # this order has just been filled on the buying side: the missing order is a sell, \n                                                    # it will be identified as missing right after: exit buy orders loop now\n                                                    break\n                                            if not self._is_just_closed_order(\n                                                next_missing_order_price, recently_closed_trades\n                                            ):\n                                                missing_orders.append(\n                                                    (next_missing_order_price, trading_enums.TradeOrderSide.BUY))\n                                            next_missing_order_price += increment\n\n                                        # create sell orders down to the highest buy order + spread\n                                        # next_missing_order_price - increment is the price of the highest buy order\n                                        spread_higher_boundary = next_missing_order_price - increment + inferred_spread\n\n                                        next_missing_order_price = order.origin_price - increment\n                                        # re-create sell orders starting from the closest sell down to spread\n                                        while next_missing_order_price >= spread_higher_boundary:\n                                            # missing sell order\n                                            if not self._is_just_closed_order(\n                                                next_missing_order_price, recently_closed_trades\n                                            ):\n                                                missing_orders.append(\n                                                    (next_missing_order_price, trading_enums.TradeOrderSide.SELL))\n                                            next_missing_order_price -= increment\n\n                                        spread = inferred_spread\n                                    else:\n                                        spread = delta_spread\n\n                                # calculations to infer ratio\n                                last_buy_cost = previous_order.origin_price * previous_order.origin_quantity\n                                first_buy_cost = sorted_orders[0].origin_price * sorted_orders[0].origin_quantity\n                                bigger_buys_closer_to_center = last_buy_cost - first_buy_cost > 0\n                                first_sell = order\n                                ratio = last_buy_cost / first_buy_cost if bigger_buys_closer_to_center \\\n                                    else first_buy_cost / last_buy_cost\n                            else:\n                                self.logger.info(f\"Current price ({self.current_price}) for {self.symbol} \"\n                                                 f\"is out of range.\")\n                                return None, self.ERROR, None\n                    if increment is None:\n                        increment = self.flat_increment or order.origin_price - previous_order.origin_price\n                        if increment <= 0:\n                            self.logger.warning(f\"Error when analyzing orders for {self.symbol}: increment <= 0.\")\n                            return None, self.ERROR, None\n                    elif not spread_point:\n                        delta_increment = order.origin_price - previous_order.origin_price\n                        # skip not-yet-updated orders\n                        if previous_order.side == order.side:\n                            missing_orders_count = float(delta_increment / increment)\n                            if missing_orders_count > 2.5 and not self._expect_missing_orders:\n                                self.logger.warning(f\"Error when analyzing orders for {self.symbol}: \"\n                                                    f\"missing_orders_count > 2.5.\")\n                                if not self._is_just_closed_order(previous_order.origin_price + increment,\n                                                                  recently_closed_trades):\n                                    return None, self.ERROR, None\n                            elif missing_orders_count > 1.5:\n                                if len(sorted_orders) < self.operational_depth and \\\n                                   (not self._skip_order_restore_on_recently_closed_orders or (\n                                       self._skip_order_restore_on_recently_closed_orders and not recently_closed_trades\n                                   )):\n                                    order_price = previous_order.origin_price + increment\n                                    while order_price < order.origin_price:\n                                        if not self._is_just_closed_order(order_price, recently_closed_trades):\n                                            missing_orders.append((order_price, order.side))\n                                        order_price += increment\n\n                    previous_order = order\n            if (only_buy or only_sell) and (increment and self.flat_spread):\n                # missing orders between others have been taken into account, now add potential missing orders\n                # on boundaries\n                # make sure that no buy order is missing from previous sell orders (or the opposite)\n                if only_buy:\n                    start_price = sorted_orders[-1].origin_price\n                    end_price = higher_bound\n                else:\n                    start_price = lower_bound\n                    end_price = sorted_orders[0].origin_price\n                missing_orders_count = float((end_price - start_price) / increment)\n                if missing_orders_count > 1.5:\n                    last_order_price = sorted_orders[-1 if only_buy else 0].origin_price\n                    same_side_order_price = last_order_price + increment if only_buy else last_order_price - increment\n                    if (\n                        # creating a new buy order <= the current price, its price is previous buy order price + increment\n                        same_side_order_price <= current_price and only_buy\n                    ) or (\n                        # creating a new sell order >= the current price, its price is previous sell order price - increment\n                        same_side_order_price >= current_price and not only_buy\n                    ):\n                        order_price = same_side_order_price\n                    else:\n                        # creating a new order on the other side, its price is taking spread into account\n                        order_price = last_order_price + self.flat_spread if only_buy else last_order_price - self.flat_spread\n                    lowest_sell = lower_bound + self.flat_spread - self.flat_increment\n                    highest_buy = higher_bound - self.flat_spread + self.flat_increment\n                    to_create_missing_orders_count = self.operational_depth - len(sorted_orders)\n\n                    while lower_bound <= order_price <= higher_bound and (\n                        self.allow_virtual_orders or len(missing_orders) < to_create_missing_orders_count\n                    ):\n                        if not self._is_just_closed_order(order_price, recently_closed_trades):\n                            side = trading_enums.TradeOrderSide.BUY if order_price < current_price \\\n                                else trading_enums.TradeOrderSide.SELL\n                            min_price = lower_bound if side == trading_enums.TradeOrderSide.BUY else lowest_sell\n                            max_price = highest_buy if side == trading_enums.TradeOrderSide.BUY else higher_bound\n                            if min_price <= order_price <= max_price:\n                                missing_orders.append((order_price, side))\n                        next_price = order_price + increment if only_buy else order_price - increment\n                        price_delta = increment\n                        if order_price <= current_price <= next_price or order_price >= current_price >= next_price:\n                            # about to switch side: apply spread\n                            price_delta = self.flat_spread\n                        order_price = order_price + price_delta if only_buy else order_price - price_delta\n\n            if ratio is not None:\n                first_sell_cost = first_sell.origin_price * first_sell.origin_quantity\n                last_sell_cost = sorted_orders[-1].origin_price * sorted_orders[-1].origin_quantity\n                bigger_sells_closer_to_center = first_sell_cost - last_sell_cost > 0\n\n                if bigger_buys_closer_to_center is not None and bigger_sells_closer_to_center is not None:\n                    if bigger_buys_closer_to_center:\n                        if bigger_sells_closer_to_center:\n                            mode = StrategyModes.NEUTRAL if 0.1 < ratio - 1 < 0.5 else StrategyModes.MOUNTAIN\n                        else:\n                            mode = StrategyModes.SELL_SLOPE\n                    else:\n                        if bigger_sells_closer_to_center:\n                            mode = StrategyModes.BUY_SLOPE\n                        else:\n                            mode = StrategyModes.VALLEY\n\n                if mode is None or increment is None or spread is None:\n                    self.logger.warning(f\"Error when analyzing orders for {self.symbol}: mode is None or increment \"\n                                        f\"is None or spread is None.\")\n                    return None, self.ERROR, None\n            if increment is None or (not (only_sell or only_buy) and spread is None):\n                self.logger.warning(f\"Error when analyzing orders for {self.symbol}: increment is None or \"\n                                    f\"(not(only_sell or only_buy) and spread is None).\")\n                return None, self.ERROR, None\n            return missing_orders, state, increment\n        else:\n            # no orders\n            return None, self.ERROR, None\n\n    def _is_just_closed_order(self, price, recently_closed_trades):\n        if not self._skip_order_restore_on_recently_closed_orders:\n            return False\n        if self.flat_increment is None:\n            return len(recently_closed_trades)\n        else:\n            inc = self.flat_spread * decimal.Decimal(\"1.5\")\n            for trade in recently_closed_trades:\n                trade_price = trade.origin_price or trade.executed_price\n                if trade_price - inc <= price <= trade_price + inc:\n                    return True\n        return False\n\n    @staticmethod\n    def _spread_in_recently_closed_order(min_amount, max_amount, sorted_closed_orders):\n        for order in sorted_closed_orders:\n            if min_amount <= order.get_origin_price() <= max_amount:\n                return True\n        return False\n\n    @staticmethod\n    def _merged_and_sort_not_virtual_orders(buy_orders, sell_orders):\n        # create sell orders first follows by buy orders\n        return StaggeredOrdersTradingModeProducer._filter_virtual_order(sell_orders) + \\\n               StaggeredOrdersTradingModeProducer._filter_virtual_order(buy_orders)\n\n    @staticmethod\n    def _filter_virtual_order(orders):\n        return [order for order in orders if not order.is_virtual]\n\n    @staticmethod\n    def _set_virtual_orders(buy_orders, sell_orders, operational_depth):\n        # all orders that are further than self.operational_depth are virtual\n        orders_count = 0\n        buy_index = 0\n        sell_index = 0\n        at_least_one_added = True\n        while orders_count < operational_depth and at_least_one_added:\n            # priority to orders closer to current price\n            at_least_one_added = False\n            if len(buy_orders) > buy_index:\n                buy_orders[buy_index].is_virtual = False\n                buy_index += 1\n                orders_count += 1\n                at_least_one_added = True\n            if len(sell_orders) > sell_index and orders_count < operational_depth:\n                sell_orders[sell_index].is_virtual = False\n                sell_index += 1\n                orders_count += 1\n                at_least_one_added = True\n\n    def _get_order_count_and_average_quantity(self, current_price, selling, lower_bound, upper_bound, holdings,\n                                              currency, mode):\n        if lower_bound >= upper_bound:\n            self.logger.error(f\"Invalid bounds for {self.symbol}: too close to the current price\")\n            return 0, 0\n        if selling:\n            order_distance = upper_bound - (lower_bound + self.flat_spread / 2)\n        else:\n            order_distance = (upper_bound - self.flat_spread / 2) - lower_bound\n        order_count_divisor = self.flat_increment\n        orders_count = math.floor(order_distance / order_count_divisor + 1) if order_count_divisor else 0\n        if orders_count < 1:\n            self.logger.warning(f\"Impossible to create {'sell' if selling else 'buy'} orders for {currency}: \"\n                                f\"not enough funds.\")\n            return 0, 0\n        if self._use_variable_orders_volume(trading_enums.TradeOrderSide.SELL if selling\n           else trading_enums.TradeOrderSide.BUY):\n            return self._ensure_average_order_quantity(orders_count, current_price, selling, holdings,\n                                                       currency, mode)\n        else:\n            return self._get_orders_count_from_fixed_volume(selling, current_price, holdings, orders_count)\n\n    def _use_variable_orders_volume(self, side):\n        return (self.sell_volume_per_order == decimal.Decimal(0) and side is trading_enums.TradeOrderSide.SELL) \\\n               or self.buy_volume_per_order == decimal.Decimal(0)\n\n    def _get_orders_count_from_fixed_volume(self, selling, current_price, holdings, orders_count):\n        volume_in_currency = self.sell_volume_per_order if selling else current_price * self.buy_volume_per_order\n        orders_count = min(math.floor(holdings / volume_in_currency), orders_count) if volume_in_currency else 0\n        return orders_count, self.sell_volume_per_order if selling else self.buy_volume_per_order\n\n    def _ensure_average_order_quantity(self, orders_count, current_price, selling,\n                                       holdings, currency, mode):\n        if not (orders_count and current_price):\n            # avoid div by 0\n            self.logger.warning(\n                f\"Can't compute average order quantity: orders_count={orders_count} and current_price={current_price}\"\n            )\n            return 0, 0\n        holdings_in_quote = holdings if selling else holdings / current_price\n        average_order_quantity = holdings_in_quote / orders_count\n        min_order_quantity, max_order_quantity = self._get_min_max_quantity(average_order_quantity, self.mode)\n        if self.min_max_order_details[self.min_quantity] is not None \\\n                and self.min_max_order_details[self.min_cost] is not None:\n            min_quantity = max(self.min_max_order_details[self.min_quantity],\n                               self.min_max_order_details[self.min_cost] / current_price)\n            min_quantity = min_quantity * decimal.Decimal(TEN_PERCENT_DECIMAL)    # increase min quantity by 10% to be sure to be\n            # able to create orders in minimal funds conditions\n            adapted_min_order_quantity = trading_personal_data.decimal_adapt_quantity(\n                self.symbol_market, min_order_quantity\n            )\n            adapted_min_quantity = trading_personal_data.decimal_adapt_quantity(self.symbol_market, min_quantity)\n            if adapted_min_order_quantity < adapted_min_quantity:\n                # 1.01 to account for order creation rounding\n                if holdings_in_quote < average_order_quantity * ONE_PERCENT_DECIMAL:\n                    return 0, 0\n                elif self.limit_orders_count_if_necessary:\n                    self.logger.warning(f\"Not enough funds to create every {self.symbol} {self.ORDERS_DESC} \"\n                                        f\"{trading_enums.TradeOrderSide.SELL.name if selling else trading_enums.TradeOrderSide.BUY.name} \"\n                                        f\"orders according to exchange's rules. Creating the maximum possible number \"\n                                        f\"of valid orders instead.\")\n                    return self._adapt_orders_count_and_quantity(holdings_in_quote, adapted_min_quantity, mode)\n                else:\n                    min_funds = self._get_min_funds(orders_count, min_quantity, self.mode, current_price)\n                    self.logger.error(f\"Impossible to create {self.symbol} {self.ORDERS_DESC} \"\n                                      f\"{trading_enums.TradeOrderSide.SELL.name if selling else trading_enums.TradeOrderSide.BUY.name} \"\n                                      f\"orders: minimum quantity for {self.mode.value} mode is lower than the minimum \"\n                                      f\"allowed for this trading pair on this exchange: requested minimum: \"\n                                      f\"{min_order_quantity} and exchange minimum is {min_quantity}. \"\n                                      f\"Minimum required funds are {min_funds}{f' {currency}' if currency else ''}.\")\n                return 0, 0\n        return orders_count, average_order_quantity\n\n    def _adapt_orders_count_and_quantity(self, holdings, min_quantity, mode):\n        # called when there are enough funds for at least one order but too many orders are requested\n        min_average_quantity = self._get_average_quantity_from_exchange_minimal_requirements(min_quantity, mode)\n        if 2 * holdings > min_average_quantity >= holdings:\n            return 1, min_average_quantity\n        max_orders_count = math.floor(holdings / min_average_quantity) if min_average_quantity else 0\n        if max_orders_count > 0:\n            # count remaining holdings if any\n            average_quantity = min_average_quantity + \\\n                               (holdings - min_average_quantity * max_orders_count) / max_orders_count\n            return max_orders_count, average_quantity\n        return 0, 0\n\n    def _get_price_from_iteration(self, starting_bound, is_selling, iteration):\n        price_step = self.flat_increment * iteration\n        price = starting_bound + price_step if is_selling else starting_bound - price_step\n        if self.min_max_order_details[self.min_price] and price < self.min_max_order_details[self.min_price]:\n            return None\n        return price\n\n    def _get_quantity_from_iteration(self, average_order_quantity, mode, side,\n                                     iteration, max_iteration, price, starting_bound):\n        multiplier_price_ratio = 1\n        min_quantity, max_quantity = self._get_min_max_quantity(average_order_quantity, mode)\n        delta = max_quantity - min_quantity\n        if max_iteration == 1:\n            quantity = average_order_quantity\n            scaled_quantity = quantity\n        else:\n            if iteration >= max_iteration:\n                raise trading_errors.NotSupported\n            iterations_progress = iteration / (max_iteration - 1)\n            if StrategyModeMultipliersDetails[mode][side] == INCREASING:\n                multiplier_price_ratio = 1 - iterations_progress\n            elif StrategyModeMultipliersDetails[mode][side] == DECREASING:\n                multiplier_price_ratio = iterations_progress\n            elif StrategyModeMultipliersDetails[mode][side] == STABLE:\n                multiplier_price_ratio = 0\n            if price <= 0:\n                return None\n            quantity = (min_quantity +\n                                   (decimal.Decimal(str(delta)) * decimal.Decimal(str(multiplier_price_ratio))))\n            # when self.quote_volume_per_order is set, keep the same volume everywhere\n            scaled_quantity = quantity * (starting_bound / price if self._use_variable_orders_volume(side)\n                                          else trading_constants.ONE)\n\n        # reduce last order quantity to avoid python float representation issues\n        if iteration == max_iteration - 1 and self._use_variable_orders_volume(side):\n            scaled_quantity = scaled_quantity * decimal.Decimal(\"0.999\")\n            quantity = quantity * decimal.Decimal(\"0.999\")\n        if self._is_valid_order_quantity_for_exchange(scaled_quantity, price):\n            return scaled_quantity\n        if self._is_valid_order_quantity_for_exchange(quantity, price):\n            return quantity\n        return None\n\n    def _is_valid_order_quantity_for_exchange(self, quantity, price):\n        if self.min_max_order_details[self.min_quantity] and (quantity < self.min_max_order_details[self.min_quantity]):\n            return False\n        cost = quantity * price\n        if self.min_max_order_details[self.min_cost] and (cost < self.min_max_order_details[self.min_cost]):\n            return False\n        return True\n\n    def _get_min_funds(self, orders_count, min_order_quantity, mode, current_price):\n        mode_multiplier = StrategyModeMultipliersDetails[mode][MULTIPLIER]\n        required_average_quantity = min_order_quantity / (1 - mode_multiplier / 2)\n\n        if self.min_cost in self.min_max_order_details:\n            average_cost = current_price * required_average_quantity\n            if self.min_max_order_details[self.min_cost]:\n                min_cost = self.min_max_order_details[self.min_cost]\n                if average_cost < min_cost:\n                    required_average_quantity = min_cost / current_price\n\n        return orders_count * required_average_quantity * TEN_PERCENT_DECIMAL\n\n    @staticmethod\n    def _get_average_quantity_from_exchange_minimal_requirements(exchange_min, mode):\n        mode_multiplier = StrategyModeMultipliersDetails[mode][MULTIPLIER]\n        # add 1% to prevent rounding issues\n        return exchange_min / (1 - mode_multiplier / 2) * ONE_PERCENT_DECIMAL\n\n    @staticmethod\n    def _get_min_max_quantity(average_order_quantity, mode):\n        mode_multiplier = StrategyModeMultipliersDetails[mode][MULTIPLIER]\n        min_quantity = average_order_quantity * (1 - mode_multiplier / 2)\n        max_quantity = average_order_quantity * (1 + mode_multiplier / 2)\n        return min_quantity, max_quantity\n\n    async def _create_order(self, order, current_price, completing_trailing, dependencies: list[str]):\n        data = {\n            StaggeredOrdersTradingModeConsumer.ORDER_DATA_KEY: order,\n            StaggeredOrdersTradingModeConsumer.CURRENT_PRICE_KEY: current_price,\n            StaggeredOrdersTradingModeConsumer.SYMBOL_MARKET_KEY: self.symbol_market,\n            StaggeredOrdersTradingModeConsumer.COMPLETING_TRAILING_KEY: completing_trailing,\n        }\n        state = trading_enums.EvaluatorStates.LONG if order.side is trading_enums.TradeOrderSide.BUY else trading_enums.EvaluatorStates.SHORT\n        await self.submit_trading_evaluation(cryptocurrency=self.trading_mode.cryptocurrency,\n                                             symbol=self.trading_mode.symbol,\n                                             time_frame=None,\n                                             state=state,\n                                             data=data,\n                                             dependencies=dependencies)\n\n    async def _create_not_virtual_orders(\n        self, orders_to_create: list, current_price: decimal.Decimal, \n        triggering_trailing: bool, dependencies: typing.Optional[commons_signals.SignalDependencies]\n    ):\n        locks_available_funds = self._should_lock_available_funds(triggering_trailing)\n        for index, order in enumerate(orders_to_create):\n            is_completing_trailing = triggering_trailing and (index == len(orders_to_create) - 1)\n            await self._create_order(order, current_price, is_completing_trailing, dependencies)\n            if locks_available_funds:\n                base, quote = symbol_util.parse_symbol(order.symbol).base_and_quote()\n                # keep track of the required funds\n                volume = order.quantity if order.side is trading_enums.TradeOrderSide.SELL \\\n                    else order.price * order.quantity\n                self._remove_from_available_funds(\n                    base if order.side is trading_enums.TradeOrderSide.SELL else quote, volume\n                )\n\n    def _refresh_symbol_data(self, symbol_market):\n        min_quantity, max_quantity, min_cost, max_cost, min_price, max_price = \\\n            trading_personal_data.get_min_max_amounts(symbol_market)\n        self.min_max_order_details[self.min_quantity] = None if min_quantity is None \\\n            else decimal.Decimal(str(min_quantity))\n        self.min_max_order_details[self.max_quantity] = None if max_quantity is None \\\n            else decimal.Decimal(str(max_quantity))\n        self.min_max_order_details[self.min_cost] = None if min_cost is None \\\n            else decimal.Decimal(str(min_cost))\n        self.min_max_order_details[self.max_cost] = None if max_cost is None \\\n            else decimal.Decimal(str(max_cost))\n        self.min_max_order_details[self.min_price] = None if min_price is None \\\n            else decimal.Decimal(str(min_price))\n        self.min_max_order_details[self.max_price] = None if max_price is None \\\n            else decimal.Decimal(str(max_price))\n\n    @classmethod\n    def get_should_cancel_loaded_orders(cls):\n        return False\n\n    def _remove_from_available_funds(self, currency, amount) -> None:\n        if self.exchange_manager.id in StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS:\n            StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[self.exchange_manager.id][currency] = \\\n                StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[self.exchange_manager.id][currency] - amount\n\n    def _set_initially_available_funds(self, currency, amount) -> None:\n        if self.exchange_manager.id not in StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS:\n            StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[self.exchange_manager.id] = {}\n        StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[self.exchange_manager.id][currency] = amount\n\n    def _is_initially_available_funds_set(self, currency) -> bool:\n        try:\n            return currency in StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[self.exchange_manager.id]\n        except KeyError:\n            return False\n\n    def _get_available_funds(self, currency) -> float:\n        try:\n            # only used when creating orders in NEW state, when NOT ignoring available funds\n            return StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[self.exchange_manager.id][currency]\n        except KeyError:\n            return 0\n\n    # syntax: \"async with xxx.get_lock():\"\n    def get_lock(self):\n        return self.lock\n"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/tests/__init__.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\n"
  },
  {
    "path": "Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport pytest\nimport copy\nimport os.path\nimport asyncio\nimport mock\nimport decimal\nimport contextlib\n\nimport async_channel.util as channel_util\n\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\nimport octobot_backtesting.api as backtesting_api\n\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as symbol_util\nimport octobot_commons.tests.test_config as test_config\nimport octobot_commons.signals as commons_signals\n\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.signals as trading_signals\nimport octobot_trading.modes\n\nimport tentacles.Trading.Mode.staggered_orders_trading_mode.staggered_orders_trading as staggered_orders_trading\n\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.memory_check_util as memory_check_util\nimport tests.test_utils.test_exchanges as test_exchanges\nimport tests.test_utils.trading_modes as test_trading_modes\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\nasync def _init_trading_mode(config, exchange_manager, symbol):\n    staggered_orders_trading.StaggeredOrdersTradingModeProducer.SCHEDULE_ORDERS_CREATION_ON_START = False\n    mode = staggered_orders_trading.StaggeredOrdersTradingMode(config, exchange_manager)\n    mode.symbol = None if mode.get_is_symbol_wildcard() else symbol\n    mode.trading_config = _get_multi_symbol_staggered_config()\n    await mode.initialize()\n    # add mode to exchange manager so that it can be stopped and freed from memory\n    exchange_manager.trading_modes.append(mode)\n    mode.producers[0].PRICE_FETCHING_TIMEOUT = 0.5\n    mode.producers[0].allow_order_funds_redispatch = True\n    test_trading_modes.set_ready_to_start(mode.producers[0])\n    return mode, mode.producers[0]\n\n\n@contextlib.asynccontextmanager\nasync def _get_tools(symbol, btc_holdings=None, additional_portfolio={}, fees=None):\n    tentacles_manager_api.reload_tentacle_info()\n    exchange_manager = None\n    try:\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USD\"] = 1000\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\n            \"BTC\"] = 10 if btc_holdings is None else btc_holdings\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO].update(additional_portfolio)\n        if fees is not None:\n            config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_SIMULATOR_FEES][\n                commons_constants.CONFIG_SIMULATOR_FEES_TAKER] = fees\n            config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_SIMULATOR_FEES][\n                commons_constants.CONFIG_SIMULATOR_FEES_MAKER] = fees\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.load_test_tentacles_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        # set BTC/USDT price at 1000 USDT\n        if symbol not in exchange_manager.client_symbols:\n            exchange_manager.client_symbols.append(symbol)\n        trading_api.force_set_mark_price(exchange_manager, symbol, 1000)\n\n        mode, producer = await _init_trading_mode(config, exchange_manager, symbol)\n\n        producer.lowest_buy = decimal.Decimal(1)\n        producer.highest_sell = decimal.Decimal(10000)\n        producer.operational_depth = 50\n        producer.spread = decimal.Decimal(\"0.06\")\n        producer.increment = decimal.Decimal(\"0.04\")\n        producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        yield producer, mode.get_trading_mode_consumers()[0], exchange_manager\n    finally:\n        if exchange_manager:\n            await _stop(exchange_manager)\n\n\n@contextlib.asynccontextmanager\nasync def _get_tools_multi_symbol():\n    exchange_manager = None\n    try:\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USD\"] = 1000\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"BTC\"] = 10\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"ETH\"] = 20\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"NANO\"] = 2000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        btc_usd_mode, btcusd_producer = await _init_trading_mode(config, exchange_manager, \"BTC/USD\")\n        eth_usdt_mode, eth_usdt_producer = await _init_trading_mode(config, exchange_manager, \"ETH/USDT\")\n        nano_usdt_mode, nano_usdt_producer = await _init_trading_mode(config, exchange_manager, \"NANO/USDT\")\n\n        btcusd_producer.lowest_buy = decimal.Decimal(1)\n        btcusd_producer.highest_sell = decimal.Decimal(10000)\n        btcusd_producer.operational_depth = 50\n        btcusd_producer.spread = decimal.Decimal(\"0.06\")\n        btcusd_producer.increment = decimal.Decimal(\"0.04\")\n        btcusd_producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        eth_usdt_producer.lowest_buy = decimal.Decimal(20)\n        eth_usdt_producer.highest_sell = decimal.Decimal(5000)\n        eth_usdt_producer.operational_depth = 30\n        eth_usdt_producer.spread = decimal.Decimal(\"0.07\")\n        eth_usdt_producer.increment = decimal.Decimal(\"0.03\")\n        eth_usdt_producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        nano_usdt_producer.lowest_buy = decimal.Decimal(20)\n        nano_usdt_producer.highest_sell = decimal.Decimal(5000)\n        nano_usdt_producer.operational_depth = 30\n        nano_usdt_producer.spread = decimal.Decimal(\"0.07\")\n        nano_usdt_producer.increment = decimal.Decimal(\"0.03\")\n        nano_usdt_producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        yield btcusd_producer, eth_usdt_producer, nano_usdt_producer, exchange_manager\n    finally:\n        if exchange_manager:\n            await _stop(exchange_manager)\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n\n\nasync def test_run_independent_backtestings_with_memory_check():\n    \"\"\"\n    Should always be called first here to avoid other tests' related memory check issues\n    \"\"\"\n    staggered_orders_trading.StaggeredOrdersTradingModeProducer.SCHEDULE_ORDERS_CREATION_ON_START = True\n    tentacles_setup_config = tentacles_manager_api.create_tentacles_setup_config_with_tentacles(\n        staggered_orders_trading.StaggeredOrdersTradingMode\n    )\n    await memory_check_util.run_independent_backtestings_with_memory_check(test_config.load_test_config(),\n                                                                           tentacles_setup_config)\n\n\nasync def test_ensure_staggered_orders():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        assert producer.current_price is None\n        # create as task to allow creator's queue to get processed\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 0))\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        with mock.patch.object(producer, \"_ensure_current_price_in_limit_parameters\", mock.Mock()) \\\n                as _ensure_current_price_in_limit_parameters_mock:\n            await producer._ensure_staggered_orders()\n            _ensure_current_price_in_limit_parameters_mock.assert_called_once()\n        # price info: create trades\n        assert producer.current_price == 4000\n        assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n\n\nasync def test_multi_symbol():\n    async with _get_tools_multi_symbol() as tools:\n        btcusd_producer, eth_usdt_producer, nano_usdt_producer, exchange_manager = tools\n        trading_api.force_set_mark_price(exchange_manager, btcusd_producer.symbol, 100)\n        await btcusd_producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, btcusd_producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len(orders) == btcusd_producer.operational_depth\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 25\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 25\n\n        trading_api.force_set_mark_price(exchange_manager, eth_usdt_producer.symbol, 200)\n        await eth_usdt_producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, btcusd_producer.operational_depth +\n                                                           eth_usdt_producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 40\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 40\n\n        trading_api.force_set_mark_price(exchange_manager, nano_usdt_producer.symbol, 200)\n        await nano_usdt_producer._ensure_staggered_orders()\n        # no new order\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, btcusd_producer.operational_depth +\n                                                           eth_usdt_producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 40\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 40\n\n        assert nano_usdt_producer._get_interfering_orders_pairs(orders) == {\"ETH/USDT\"}\n\n        # new ETH USDT evaluation, price changed\n        # -2 order would be filled\n        original_orders = copy.copy(orders)\n        to_fill_order = original_orders[-2]\n        await _fill_order(to_fill_order, exchange_manager, producer=eth_usdt_producer)\n        # filled order and created a new one\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, len(original_orders)))\n        trading_api.force_set_mark_price(exchange_manager, eth_usdt_producer.symbol, 190)\n        await nano_usdt_producer._ensure_staggered_orders()\n        # did nothing\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, len(original_orders)))\n    assert staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS == {}\n\n\nasync def test_available_funds_management():\n    async with _get_tools_multi_symbol() as tools:\n        btcusd_producer, eth_usdt_producer, nano_usdt_producer, exchange_manager = tools\n        assert staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS == {}\n\n        trading_api.force_set_mark_price(exchange_manager, btcusd_producer.symbol, 100)\n        await btcusd_producer._ensure_staggered_orders()\n        available_funds = \\\n            staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS[exchange_manager.id]\n        assert len(available_funds) == 2\n        btc_available_funds = available_funds[\"BTC\"]\n        usd_available_funds = available_funds[\"USD\"]\n        assert btc_available_funds < decimal.Decimal(\"9.9\")\n        assert usd_available_funds < decimal.Decimal(\"31\")\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, btcusd_producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        pf_btc_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        # ensure there at least the same (or more) actual portfolio available funds than on the producer value\n        # (due to exchange rounding reducing some amounts)\n        assert pf_btc_available_funds * decimal.Decimal(\"0.999\") <= btc_available_funds <= pf_btc_available_funds\n        pf_usd_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert pf_usd_available_funds * decimal.Decimal(\"0.999\") <= usd_available_funds <= pf_usd_available_funds\n        assert len(orders) == btcusd_producer.operational_depth\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 25\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 25\n\n        trading_api.force_set_mark_price(exchange_manager, eth_usdt_producer.symbol, 200)\n        await eth_usdt_producer._ensure_staggered_orders()\n        assert len(available_funds) == 4\n        # did not change previous funds\n        assert btc_available_funds == available_funds[\"BTC\"]\n        assert usd_available_funds == available_funds[\"USD\"]\n        eth_available_funds = available_funds[\"ETH\"]\n        usdt_available_funds = available_funds[\"USDT\"]\n        assert eth_available_funds < decimal.Decimal(\"19.6\")\n        assert usdt_available_funds < decimal.Decimal(\"753\")\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, btcusd_producer.operational_depth +\n                                                           eth_usdt_producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        pf_eth_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"ETH\").available\n        assert pf_eth_available_funds * decimal.Decimal(\"0.999\") <= eth_available_funds <= pf_eth_available_funds\n        pf_usdt_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USDT\").available\n        assert pf_usdt_available_funds * decimal.Decimal(\"0.999\") <= usdt_available_funds <= pf_usdt_available_funds\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 40\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 40\n\n        trading_api.force_set_mark_price(exchange_manager, nano_usdt_producer.symbol, 200)\n        await nano_usdt_producer._ensure_staggered_orders()\n        # did not change available funds\n        assert len(available_funds) == 4\n        assert btc_available_funds == available_funds[\"BTC\"]\n        assert usd_available_funds == available_funds[\"USD\"]\n        assert eth_available_funds == available_funds[\"ETH\"]\n        assert usdt_available_funds == available_funds[\"USDT\"]\n        # no new order\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, btcusd_producer.operational_depth +\n                                                           eth_usdt_producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 40\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 40\n\n        assert nano_usdt_producer._get_interfering_orders_pairs(orders) == {\"ETH/USDT\"}\n\n        # new ETH USDT evaluation, price changed\n        # -2 order would be filled\n        original_orders = copy.copy(orders)\n        to_fill_order = original_orders[-2]\n        await _fill_order(to_fill_order, exchange_manager, producer=eth_usdt_producer)\n        trading_api.force_set_mark_price(exchange_manager, eth_usdt_producer.symbol, 190)\n        await nano_usdt_producer._ensure_staggered_orders()\n        # did nothing\n        # did not change available funds\n        assert len(available_funds) == 4\n        assert btc_available_funds == available_funds[\"BTC\"]\n        assert usd_available_funds == available_funds[\"USD\"]\n        assert eth_available_funds == available_funds[\"ETH\"]\n        assert usdt_available_funds == available_funds[\"USDT\"]\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, len(original_orders)))\n    # clear available funds\n    assert staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS == {}\n\n\nasync def test_ensure_staggered_orders_with_target_sell_and_buy_funds():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n\n        producer.sell_funds = decimal.Decimal(\"0.001\")\n        producer.buy_funds = decimal.Decimal(100)\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        await producer._ensure_staggered_orders()\n        btc_available_funds = producer._get_available_funds(\"BTC\")\n        usd_available_funds = producer._get_available_funds(\"USD\")\n        # btc_available_funds for reduced because orders are not created\n        assert 10 - 0.001 <= btc_available_funds < 10\n        assert 1000 - 100 <= usd_available_funds < 1000\n        # price info: create trades\n        assert producer.current_price == 4000\n        assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        pf_btc_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pf_usd_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert pf_btc_available_funds >= 9.999\n        assert pf_usd_available_funds >= 900\n\n        assert pf_btc_available_funds >= btc_available_funds\n        assert pf_usd_available_funds >= usd_available_funds\n\n\nasync def test_ensure_staggered_orders_with_unavailable_funds():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n\n        producer._set_initially_available_funds(\"BTC\", decimal.Decimal(1))\n        producer._set_initially_available_funds(\"USD\", decimal.Decimal(400))\n\n        # set BTC/USD price at 4000 USD\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        await producer._ensure_staggered_orders()\n        btc_available_funds = producer._get_available_funds(\"BTC\")\n        usd_available_funds = producer._get_available_funds(\"USD\")\n        # btc_available_funds for reduced because orders are not created\n        assert btc_available_funds < 1\n        assert usd_available_funds < 400\n        # price info: create trades\n        assert producer.current_price == 4000\n        assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        pf_btc_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        pf_usd_available_funds = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert pf_btc_available_funds >= 9\n        assert pf_usd_available_funds >= 600\n\n        # - 9 to make it as if itr was starting with 1 btc (to compare with btc_available_funds)\n        assert pf_btc_available_funds - 9 >= btc_available_funds\n        # - 600 to make it as if itr was starting with 1 btc (to compare with btc_available_funds)\n        assert pf_usd_available_funds - 600 >= usd_available_funds\n\n\nasync def test_get_maximum_traded_funds():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n\n        # part 1: no available funds set\n        # no allowed_funds set\n        # can trade total_available_funds\n        assert producer._get_maximum_traded_funds(0, 10, \"BTC\", True, False) == 10 == decimal.Decimal(10)\n        # allowed_funds set\n        # can trade allowed_funds\n        assert producer._get_maximum_traded_funds(5, 10, \"BTC\", False, False) == 5\n        # allowed_funds set, allowed_funds larger than total_available_funds\n        # can trade total_available_funds\n        assert producer._get_maximum_traded_funds(15, 10, \"BTC\", True, False) == 10\n\n        # part 2: available funds set is set\n        producer._set_initially_available_funds(\"BTC\", decimal.Decimal(8))\n        # no allowed_funds set\n        # can trade available funds only\n        assert producer._get_maximum_traded_funds(0, 10, \"BTC\", False, False) == 8\n        # allowed_funds set\n        # can trade allowed_funds (lower than available funds)\n        assert producer._get_maximum_traded_funds(5, 10, \"BTC\", True, False) == 5\n        # allowed_funds set, allowed_funds larger than total_available_funds\n        # can trade available funds only\n        assert producer._get_maximum_traded_funds(15, 10, \"BTC\", False, False) == 8\n\n\nasync def test_get_new_state_price():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.current_price = 4000\n        assert producer._get_new_state_price() == 4000\n\n        producer.starting_price = 2\n        assert producer._get_new_state_price() == 2\n\n\nasync def test_set_increment_and_spread():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        assert producer.flat_increment is None\n        assert producer.flat_spread is None\n        producer._set_increment_and_spread(1000)\n        assert producer.flat_increment == 1000 * producer.increment\n        assert producer.flat_spread == 1000 * producer.spread\n\n        producer._set_increment_and_spread(2000)\n        # no change: producer.flat_increment and producer.flat_spread are not None\n        assert producer.flat_increment == 1000 * producer.increment\n        assert producer.flat_spread == 1000 * producer.spread\n\n        # reset\n        producer.flat_increment = None\n        producer.flat_spread = None\n        # use candidate_flat_increment\n        producer._set_increment_and_spread(3000, candidate_flat_increment=500)\n        assert producer.flat_increment == 500\n        assert producer.flat_spread == 500 * producer.spread / producer.increment\n\n\nasync def test_use_existing_orders_only():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer.use_existing_orders_only = True\n        assert producer.flat_increment is None\n        assert producer.flat_spread is None\n        with mock.patch.object(producer, '_create_order', new=mock.AsyncMock()) as mocked_producer_create_order:\n            trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n            await producer._ensure_staggered_orders()\n            # price info: create trades\n            assert producer.current_price == 4000\n            assert producer.state == trading_enums.EvaluatorStates.NEUTRAL\n            mocked_producer_create_order.assert_not_called()\n        assert producer.flat_increment is not None\n        assert producer.flat_spread is not None\n        await asyncio.create_task(_wait_for_orders_creation(2))\n        # did not create orders\n        assert not trading_api.get_open_orders(exchange_manager)\n\n\nasync def test_create_orders_without_existing_orders_symmetrical_case_all_modes_price_100():\n    price = 100\n    await _test_mode(staggered_orders_trading.StrategyModes.NEUTRAL, 25, 2475, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.MOUNTAIN, 25, 2475, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.VALLEY, 25, 2475, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.BUY_SLOPE, 25, 2475, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.SELL_SLOPE, 25, 2475, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.FLAT, 25, 2475, price)\n\n\nasync def test_create_orders_without_existing_orders_symmetrical_case_all_modes_price_347():\n    price = 347\n    await _test_mode(staggered_orders_trading.StrategyModes.NEUTRAL, 25, 695, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.MOUNTAIN, 25, 695, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.VALLEY, 25, 695, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.BUY_SLOPE, 25, 695, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.SELL_SLOPE, 25, 695, price)\n    await _test_mode(staggered_orders_trading.StrategyModes.FLAT, 25, 695, price)\n\n\nasync def test_create_orders_without_existing_orders_symmetrical_case_all_modes_price_0_347():\n    price = 0.347\n    lowest_buy = 0.001\n    highest_sell = 400\n    btc_holdings = 400\n    await _test_mode(staggered_orders_trading.StrategyModes.NEUTRAL, 25, 28793, price, lowest_buy, highest_sell,\n                     btc_holdings)\n    await _test_mode(staggered_orders_trading.StrategyModes.MOUNTAIN, 25, 28793, price, lowest_buy, highest_sell,\n                     btc_holdings)\n    await _test_mode(staggered_orders_trading.StrategyModes.VALLEY, 25, 28793, price, lowest_buy, highest_sell,\n                     btc_holdings)\n    await _test_mode(staggered_orders_trading.StrategyModes.BUY_SLOPE, 25, 28793, price, lowest_buy, highest_sell,\n                     btc_holdings)\n    await _test_mode(staggered_orders_trading.StrategyModes.SELL_SLOPE, 25, 28793, price, lowest_buy, highest_sell,\n                     btc_holdings)\n    await _test_mode(staggered_orders_trading.StrategyModes.FLAT, 25, 28793, price, lowest_buy, highest_sell,\n                     btc_holdings)\n\n\nasync def test_create_orders_from_different_markets():\n    async with _get_tools(\"BTC/USD\", additional_portfolio={\"RDN\": 6740, \"ETH\": 10}) as tools:\n        producer, _, exchange_manager = tools\n        producer.symbol = \"RDN/ETH\"\n\n        price = 0.0024161\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer.current_price = decimal.Decimal(str(price))\n        producer._refresh_symbol_data(symbol_market)\n        producer.min_max_order_details[producer.min_cost] = decimal.Decimal(str(0.01))\n        producer.min_max_order_details[producer.min_quantity] = decimal.Decimal(str(1.0))\n        producer.min_max_order_details[producer.max_quantity] = decimal.Decimal(str(90000000.0))\n        producer.min_max_order_details[producer.max_cost] = None\n        producer.min_max_order_details[producer.max_price] = None\n        producer.min_max_order_details[producer.min_price] = None\n\n        # await _test_mode(staggered_orders_trading.StrategyModes.NEUTRAL, 0, 0, price)\n        lowest_buy = 0.0013\n        highest_sell = 0.0043\n        expected_buy_count = 46\n        expected_sell_count = 78\n\n        producer.lowest_buy = decimal.Decimal(str(lowest_buy))\n        producer.highest_sell = decimal.Decimal(str(highest_sell))\n        producer.increment = decimal.Decimal(str(0.01))\n        producer.spread = decimal.Decimal(str(0.01))\n        producer.operational_depth = 10\n        producer.final_eval = price\n        producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        await _light_check_orders(producer, exchange_manager, expected_buy_count, expected_sell_count, price)\n\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == producer.operational_depth\n\n        # test trigger refresh\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, 0.0024161)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        # did nothing\n        assert original_orders[0] is trading_api.get_open_orders(exchange_manager)[0]\n        assert original_orders[-1] is trading_api.get_open_orders(exchange_manager)[-1]\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth\n\n\nasync def test_create_orders_from_different_very_close_refresh():\n    async with _get_tools(\"BTC/USD\", additional_portfolio={\"RDN\": 6740, \"ETH\": 10}) as tools:\n        producer, _, exchange_manager = tools\n        producer.symbol = \"RDN/ETH\"\n        price = 0.00231\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer.current_price = price\n        producer._refresh_symbol_data(symbol_market)\n        producer.min_max_order_details[producer.min_cost] = decimal.Decimal(str(0.01))\n        producer.min_max_order_details[producer.min_quantity] = decimal.Decimal(str(1.0))\n        producer.min_max_order_details[producer.max_quantity] = decimal.Decimal(str(90000000.0))\n        producer.min_max_order_details[producer.max_cost] = None\n        producer.min_max_order_details[producer.max_price] = None\n        producer.min_max_order_details[producer.min_price] = None\n\n        # await _test_mode(staggered_orders_trading.StrategyModes.NEUTRAL, 0, 0, price)\n        lowest_buy = 0.00221\n        highest_sell = 0.00242\n        expected_buy_count = 2\n        expected_sell_count = 2\n\n        producer.lowest_buy = decimal.Decimal(str(lowest_buy))\n        producer.highest_sell = decimal.Decimal(str(highest_sell))\n        producer.increment = decimal.Decimal(str(0.02))\n        producer.spread = decimal.Decimal(str(0.02))\n        producer.operational_depth = 10\n        producer.final_eval = price\n        producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        await _light_check_orders(producer, exchange_manager, expected_buy_count, expected_sell_count, price)\n\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        original_length = len(original_orders)\n\n        # test trigger refresh\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, 0.0023185)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        # did nothing\n        assert original_orders[0] is trading_api.get_open_orders(exchange_manager)[0]\n        assert original_orders[-1] is trading_api.get_open_orders(exchange_manager)[-1]\n        assert original_length == len(trading_api.get_open_orders(exchange_manager))\n\n        # test more trigger refresh\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, 0.0022991)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        # did nothing\n        assert original_orders[0] is trading_api.get_open_orders(exchange_manager)[0]\n        assert original_orders[-1] is trading_api.get_open_orders(exchange_manager)[-1]\n        assert original_length == len(trading_api.get_open_orders(exchange_manager))\n\n\nasync def test_create_orders_from_different_markets_not_enough_market_to_create_all_orders():\n    async with _get_tools(\"BTC/USD\", additional_portfolio={\"RDN\": 6740, \"ETH\": 10}) as tools:\n        producer, _, exchange_manager = tools\n        producer.symbol = \"RDN/ETH\"\n        price = 0.0024161\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer.current_price = price\n        producer._refresh_symbol_data(symbol_market)\n        producer.min_max_order_details[producer.min_cost] = decimal.Decimal(str(1.0))\n        producer.min_max_order_details[producer.min_quantity] = decimal.Decimal(str(1.0))\n        producer.min_max_order_details[producer.max_quantity] = decimal.Decimal(str(90000000.0))\n        producer.min_max_order_details[producer.max_cost] = None\n        producer.min_max_order_details[producer.max_price] = None\n        producer.min_max_order_details[producer.min_price] = None\n\n        # await _test_mode(staggered_orders_trading.StrategyModes.NEUTRAL, 0, 0, price)\n        lowest_buy = 0.0013\n        highest_sell = 0.0043\n        expected_buy_count = 0\n        expected_sell_count = 0\n\n        producer.lowest_buy = decimal.Decimal(str(lowest_buy))\n        producer.highest_sell = decimal.Decimal(str(highest_sell))\n        producer.increment = decimal.Decimal(str(0.01))\n        producer.spread = decimal.Decimal(str(0.01))\n        producer.operational_depth = 10\n        producer.final_eval = price\n        producer.mode = staggered_orders_trading.StrategyModes.MOUNTAIN\n\n        await _light_check_orders(producer, exchange_manager, expected_buy_count, expected_sell_count, price)\n\n\nasync def test_start_with_existing_valid_orders():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == producer.operational_depth\n\n        # new evaluation, same price\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        # did nothing\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        assert original_orders[0] is trading_api.get_open_orders(exchange_manager)[0]\n        assert original_orders[-1] is trading_api.get_open_orders(exchange_manager)[-1]\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth\n        first_buy_index = int(len(trading_api.get_open_orders(exchange_manager)) / 2)\n\n        # new evaluation, price changed\n        # -2 order would be filled\n        to_fill_order = original_orders[first_buy_index]\n        price = 95\n        await _fill_order(to_fill_order, exchange_manager, price, producer=producer)\n        await asyncio.create_task(_wait_for_orders_creation(2))\n        # did nothing: orders got replaced\n        assert len(original_orders) == len(trading_api.get_open_orders(exchange_manager))\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        # did nothing\n        assert len(original_orders) == len(trading_api.get_open_orders(exchange_manager))\n\n        # orders gets cancelled\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        to_cancel = [open_orders[20], open_orders[18], open_orders[3]]\n        for order in to_cancel:\n            await exchange_manager.trader.cancel_order(order)\n        post_available = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(to_cancel)\n\n        producer.RECENT_TRADES_ALLOWED_TIME = -1\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        # restored orders\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USD\").available <= post_available\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n\nasync def test_price_initially_out_of_range_1():\n    async with _get_tools(\"BTC/USD\", btc_holdings=100000000) as tools:\n        producer, _, exchange_manager = tools\n        # new evaluation: price in range\n        # ~300k sell orders, 0 buy orders\n        price = 0.8\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == producer.operational_depth\n        assert all(o.side == trading_enums.TradeOrderSide.SELL for o in original_orders)\n        assert all(producer.highest_sell >= o.origin_price >= producer.lowest_buy\n                   for o in original_orders)\n\n\nasync def test_price_initially_out_of_range_2():\n    async with _get_tools(\"BTC/USD\", btc_holdings=10000000) as tools:\n        producer, _, exchange_manager = tools\n        # new evaluation: price in range\n        price = 100000\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == 2\n        assert all(o.side == trading_enums.TradeOrderSide.BUY for o in original_orders)\n        assert all(producer.highest_sell >= o.origin_price >= producer.lowest_buy\n                   for o in original_orders)\n\n\nasync def test_price_going_out_of_range():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        # new evaluation: price in range\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n\n        # new evaluation: price out of range: >\n        price = 100000\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        producer.current_price = price\n        existing_orders = trading_api.get_open_orders(exchange_manager)\n        sorted_orders = sorted(existing_orders, key=lambda order: order.origin_price)\n        missing_orders, state, candidate_flat_increment = producer._analyse_current_orders_situation(\n            sorted_orders, [], sorted_orders[0].origin_price, sorted_orders[-1].origin_price, price\n        )\n        assert missing_orders is None\n        assert candidate_flat_increment is None\n        assert state == producer.ERROR\n\n        # new evaluation: price out of range: <\n        price = 0.1\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        producer.current_price = price\n        existing_orders = trading_api.get_open_orders(exchange_manager)\n        sorted_orders = sorted(existing_orders, key=lambda order: order.origin_price)\n        missing_orders, state, candidate_flat_increment = producer._analyse_current_orders_situation(\n            sorted_orders, [], sorted_orders[0].origin_price, sorted_orders[-1].origin_price, price\n        )\n        assert missing_orders is None\n        assert candidate_flat_increment is None\n        assert state == producer.ERROR\n\n\nasync def test_start_after_offline_filled_orders():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        # first start: setup orders\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        original_orders = copy.copy(trading_api.get_open_orders(exchange_manager))\n        assert len(original_orders) == producer.operational_depth\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if 90 <= o.origin_price <= 110]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(offline_filled)\n\n        # back online: restore orders according to current price\n        price = 96\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        # force not use recent trades\n        producer.RECENT_TRADES_ALLOWED_TIME = -1\n        # force funds reset in this test\n        with mock.patch.object(\n            producer, \"_ensure_full_funds_usage\", side_effect=staggered_orders_trading.ForceResetOrdersException\n        ) as mock_ensure_full_funds_usage:\n            await producer._ensure_staggered_orders()\n            mock_ensure_full_funds_usage.assert_called_once()\n            # restored orders\n            await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n            assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USD\").available <= post_portfolio\n            assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n\nasync def test_health_check_during_filled_orders():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        # first start: setup orders\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n\n        # offline simulation: orders get filled but not replaced => price got up to 110 and not down to 90, now is 96s\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        offline_filled = [o for o in open_orders if 90 <= o.origin_price <= 110]\n        for order in offline_filled:\n            await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer)\n        post_portfolio = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert pre_portfolio < post_portfolio\n        assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(offline_filled)\n\n        # back online: restore orders according to current price\n        price = 96\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        # did not restore orders: they are being closed and callback will proceed (considered as recently closed\n        # and consumer in queue)\n        await asyncio.create_task(\n            _check_open_orders_count(exchange_manager, producer.operational_depth - len(offline_filled)))\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"USD\").available <= post_portfolio\n        assert 0 <= trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n\n\nasync def test_compute_minimum_funds_1():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        # first start: setup orders\n        buy_min_funds = producer._get_min_funds(decimal.Decimal(str(25)), decimal.Decimal(str(0.001)),\n                                                staggered_orders_trading.StrategyModes.MOUNTAIN,\n                                                decimal.Decimal(100))\n        sell_min_funds = producer._get_min_funds(decimal.Decimal(str(2475.25)), decimal.Decimal(str(0.00001)),\n                                                 staggered_orders_trading.StrategyModes.MOUNTAIN,\n                                                 decimal.Decimal(100))\n        assert buy_min_funds == decimal.Decimal(str(0.05)) * staggered_orders_trading.TEN_PERCENT_DECIMAL\n        assert sell_min_funds == decimal.Decimal(str(0.049505)) * staggered_orders_trading.TEN_PERCENT_DECIMAL\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USD\").available = buy_min_funds\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USD\").total = buy_min_funds\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = sell_min_funds\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = sell_min_funds\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len(orders) == producer.operational_depth\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 25\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.BUY]) == 25\n\n\nasync def test_compute_minimum_funds_2():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        # first start: setup orders\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer._refresh_symbol_data(symbol_market)\n        buy_min_funds = producer._get_min_funds(decimal.Decimal(str(25)), decimal.Decimal(str(0.001)),\n                                                staggered_orders_trading.StrategyModes.MOUNTAIN,\n                                                decimal.Decimal(str(100)))\n        sell_min_funds = producer._get_min_funds(decimal.Decimal(str(2475)), decimal.Decimal(str(0.00001)),\n                                                 staggered_orders_trading.StrategyModes.MOUNTAIN,\n                                                 decimal.Decimal(str(100)))\n        assert buy_min_funds == decimal.Decimal(str(0.05)) * staggered_orders_trading.TEN_PERCENT_DECIMAL\n        assert sell_min_funds == decimal.Decimal(str(0.0495)) * staggered_orders_trading.TEN_PERCENT_DECIMAL\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USD\").available = buy_min_funds * decimal.Decimal(\"0.99999\")\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"USD\").total = buy_min_funds * decimal.Decimal(\"0.99999\")\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").available = sell_min_funds * decimal.Decimal(\"0.99999\")\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\"BTC\").total = sell_min_funds * decimal.Decimal(\"0.99999\")\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 0))\n\n\nasync def test_start_without_enough_funds_to_buy():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n            \"USD\").available = decimal.Decimal(\"0.00005\")\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n            \"USD\").total = decimal.Decimal(\"0.00005\")\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len(orders) == producer.operational_depth\n        assert all([o.side == trading_enums.TradeOrderSide.SELL for o in orders])\n\n        # trigger health check\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n\n        await _fill_order(orders[5], exchange_manager, producer=producer)\n\n\nasync def test_start_without_enough_funds_to_sell():\n    async with _get_tools(\"BTC/USD\", btc_holdings=0.00001) as tools:\n        producer, _, exchange_manager = tools\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len(orders) == 25\n        assert all([o.side == trading_enums.TradeOrderSide.BUY for o in orders])\n\n        # trigger health check\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n\n        # check order fill callback recreates spread\n        to_fill_order = orders[5]\n        second_to_fill_order = orders[4]\n        await _fill_order(to_fill_order, exchange_manager, producer=producer)\n        await asyncio.create_task(_wait_for_orders_creation(2))\n        orders = trading_api.get_open_orders(exchange_manager)\n        newly_created_sell_order = orders[-1]\n        assert newly_created_sell_order.side == trading_enums.TradeOrderSide.SELL\n        assert newly_created_sell_order.origin_price == to_fill_order.origin_price + \\\n               producer.flat_spread - producer.flat_increment\n\n        await _fill_order(second_to_fill_order, exchange_manager, producer=producer)\n        await asyncio.create_task(_wait_for_orders_creation(2))\n        orders = trading_api.get_open_orders(exchange_manager)\n        second_newly_created_sell_order = orders[-1]\n        assert second_newly_created_sell_order.side == trading_enums.TradeOrderSide.SELL\n        assert second_newly_created_sell_order.origin_price == second_to_fill_order.origin_price + \\\n               producer.flat_spread - producer.flat_increment\n        assert abs(second_newly_created_sell_order.origin_price - newly_created_sell_order.origin_price) == \\\n               producer.flat_increment\n\n\nasync def test_start_without_enough_funds_at_all():\n    async with _get_tools(\"BTC/USD\", btc_holdings=0.00001) as tools:\n        producer, _, exchange_manager = tools\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n            \"USD\").available = decimal.Decimal(\"0.00005\")\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.get_currency_portfolio(\n            \"USD\").total = decimal.Decimal(\"0.00005\")\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, 0))\n\n\nasync def test_settings_for_just_one_order_on_a_side():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        producer.highest_sell = 106\n        price = 100\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        orders = trading_api.get_open_orders(exchange_manager)\n        assert len([o for o in orders if o.side == trading_enums.TradeOrderSide.SELL]) == 1\n\n\nasync def test_order_fill_callback():\n    async with _get_tools(\"BTC/USD\", fees=0) as tools:\n        producer, _, exchange_manager = tools\n        # create orders\n        price = 100\n        producer.mode = staggered_orders_trading.StrategyModes.NEUTRAL\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        previous_total = _get_total_usd(exchange_manager, 100)\n\n        now_btc = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").total\n        now_usd = trading_api.get_portfolio_currency(exchange_manager, \"USD\").total\n\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n        price_increment = producer.flat_increment\n        price_spread = producer.flat_spread\n\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        assert len(open_orders) == producer.operational_depth\n\n        # closest to centre buy order is filled => bought btc\n        to_fill_order = open_orders[-2]\n        await _fill_order(to_fill_order, exchange_manager, producer=producer)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n\n        # instantly create sell order at price * (1 + increment)\n        assert len(open_orders) == producer.operational_depth\n        assert to_fill_order not in open_orders\n        newly_created_sell_order = open_orders[-1]\n        assert newly_created_sell_order.associated_entry_ids == [to_fill_order.order_id]\n        assert newly_created_sell_order.symbol == to_fill_order.symbol\n        price = to_fill_order.origin_price + (price_spread - price_increment)\n        assert newly_created_sell_order.origin_price == trading_personal_data.decimal_trunc_with_n_decimal_digits(price,\n                                                                                                                  8)\n        assert newly_created_sell_order.origin_quantity == \\\n               trading_personal_data.decimal_trunc_with_n_decimal_digits(\n                   to_fill_order.filled_quantity * (1 - producer.max_fees),8)\n        assert newly_created_sell_order.side == trading_enums.TradeOrderSide.SELL\n        assert trading_api.get_portfolio_currency(exchange_manager, \"BTC\").total > now_btc\n        now_btc = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").total\n        current_total = _get_total_usd(exchange_manager, 100)\n        assert previous_total < current_total\n        previous_total_buy = current_total\n\n        # now this new sell order is filled => sold btc\n        to_fill_order = open_orders[-1]\n        await _fill_order(to_fill_order, exchange_manager, producer=producer)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n\n        # instantly create buy order at price * (1 + increment)\n        assert len(open_orders) == producer.operational_depth\n        assert to_fill_order not in open_orders\n        newly_created_buy_order = open_orders[-1]\n        assert newly_created_buy_order.associated_entry_ids is None # buy order => previous sell order is not an entry\n        assert newly_created_buy_order.symbol == to_fill_order.symbol\n        price = to_fill_order.origin_price - (price_spread - price_increment)\n        assert newly_created_buy_order.origin_price == trading_personal_data.decimal_trunc_with_n_decimal_digits(price, 8)\n        assert newly_created_buy_order.origin_quantity == \\\n               trading_personal_data.decimal_trunc_with_n_decimal_digits(\n                   to_fill_order.filled_price / price * to_fill_order.filled_quantity * (1 - producer.max_fees), 8)\n        assert newly_created_buy_order.side == trading_enums.TradeOrderSide.BUY\n        assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").total > now_usd\n        now_usd = trading_api.get_portfolio_currency(exchange_manager, \"USD\").total\n        current_total = _get_total_usd(exchange_manager, 100)\n        assert previous_total < current_total\n        previous_total_sell = current_total\n\n        # now this new buy order is filled => bought btc\n        to_fill_order = open_orders[-1]\n        await _fill_order(to_fill_order, exchange_manager, producer=producer)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n\n        # instantly create sell order at price * (1 + increment)\n        assert len(open_orders) == producer.operational_depth\n        assert to_fill_order not in open_orders\n        newly_created_sell_order = open_orders[-1]\n        assert newly_created_sell_order.associated_entry_ids == [to_fill_order.order_id]\n        assert newly_created_sell_order.symbol == to_fill_order.symbol\n        price = to_fill_order.origin_price + (price_spread - price_increment)\n        assert newly_created_sell_order.origin_price == trading_personal_data.decimal_trunc_with_n_decimal_digits(price, 8)\n        assert newly_created_sell_order.origin_quantity == \\\n               trading_personal_data.decimal_trunc_with_n_decimal_digits(\n                   to_fill_order.filled_quantity * (1 - producer.max_fees),\n                   8)\n        assert newly_created_sell_order.side == trading_enums.TradeOrderSide.SELL\n        assert trading_api.get_portfolio_currency(exchange_manager, \"BTC\").total > now_btc\n        current_total = _get_total_usd(exchange_manager, 100)\n        assert previous_total_buy < current_total\n\n        # now this new sell order is filled => sold btc\n        to_fill_order = open_orders[-1]\n        await _fill_order(to_fill_order, exchange_manager, producer=producer)\n        open_orders = trading_api.get_open_orders(exchange_manager)\n\n        # instantly create buy order at price * (1 + increment)\n        assert len(open_orders) == producer.operational_depth\n        assert to_fill_order not in open_orders\n        newly_created_buy_order = open_orders[-1]\n        assert newly_created_buy_order.associated_entry_ids is None # buy order => previous sell order is not an entry\n        assert newly_created_buy_order.symbol == to_fill_order.symbol\n        price = to_fill_order.origin_price - (price_spread - price_increment)\n        assert newly_created_buy_order.origin_price == trading_personal_data.decimal_trunc_with_n_decimal_digits(price, 8)\n        assert newly_created_buy_order.origin_quantity == \\\n               trading_personal_data.decimal_trunc_with_n_decimal_digits(\n                   to_fill_order.filled_price / price * to_fill_order.filled_quantity * (1 - producer.max_fees),\n                   8)\n        assert newly_created_buy_order.side == trading_enums.TradeOrderSide.BUY\n        assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").total > now_usd\n        current_total = _get_total_usd(exchange_manager, 100)\n        assert previous_total_sell < current_total\n\n\nasync def test_order_fill_callback_with_mirror_delay():\n    async with _get_tools(\"BTC/USD\", fees=0) as tools:\n        producer, _, exchange_manager = tools\n        # create orders\n        price = 100\n        producer.mode = staggered_orders_trading.StrategyModes.NEUTRAL\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        assert len(open_orders) == producer.operational_depth\n\n        # closest to centre buy order is filled => bought btc\n        producer.mirror_order_delay = 0.1\n        to_fill_order = open_orders[-2]\n        in_backtesting = \"tentacles.Trading.Mode.staggered_orders_trading_mode.staggered_orders_trading.trading_api.get_is_backtesting\"\n        with mock.patch(in_backtesting, return_value=False), \\\n             mock.patch.object(producer, \"_create_order\") as producer_create_order_mock:\n            await _fill_order(to_fill_order, exchange_manager, producer=producer)\n            assert len(producer.mirror_orders_tasks)\n            producer_create_order_mock.assert_not_called()\n            await asyncio.sleep(0.05)\n            producer_create_order_mock.assert_not_called()\n            await asyncio.sleep(0.1)\n            producer_create_order_mock.assert_called_once()\n\n\nasync def test_compute_mirror_order_volume():\n    async with _get_tools(\"BTC/USD\", fees=0) as tools:\n        producer, _, exchange_manager = tools\n        # no ignore_exchange_fees\n        # no fixed volumes\n        producer.ignore_exchange_fees = False\n        # 1% max fees\n        producer.max_fees = decimal.Decimal(\"0.01\")\n        # take exchange fees into account\n        assert producer._compute_mirror_order_volume(\n            True, decimal.Decimal(\"100\"), decimal.Decimal(\"120\"), decimal.Decimal(\"2\"), None\n        ) == 2 * (1 - producer.max_fees)\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), {}\n        ) == 2 * (decimal.Decimal(\"100\") / decimal.Decimal(\"80\")) * (1 - producer.max_fees)\n        # with given fees\n        fees = {\n            trading_enums.FeePropertyColumns.COST.value: decimal.Decimal(\"0.032\"),\n            trading_enums.FeePropertyColumns.CURRENCY.value: \"BTC\"\n        }\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), fees\n        ) == 2 * (decimal.Decimal(\"100\") / decimal.Decimal(\"80\")) - decimal.Decimal(\"0.032\")\n        fees = {\n            trading_enums.FeePropertyColumns.COST.value: decimal.Decimal(\"2.3\"),\n            trading_enums.FeePropertyColumns.CURRENCY.value: \"USD\"\n        }\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), fees\n        ) == 2 * (decimal.Decimal(\"100\") / decimal.Decimal(\"80\")) - (decimal.Decimal(\"2.3\") / decimal.Decimal(\"100\"))\n\n        # with ignore_exchange_fees\n        producer.ignore_exchange_fees = True\n        # consider fees already taken, sell everything\n        assert producer._compute_mirror_order_volume(\n            True, decimal.Decimal(\"100\"), decimal.Decimal(\"120\"), decimal.Decimal(\"2\"), None\n        ) == 2\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), {}\n        ) == 2 * (decimal.Decimal(\"100\") / decimal.Decimal(\"80\"))\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), fees\n        ) == 2 * (decimal.Decimal(\"100\") / decimal.Decimal(\"80\"))\n\n        # with fixed volumes\n        producer.ignore_exchange_fees = False\n        producer.sell_volume_per_order = 3\n        # consider fees already taken, sell everything\n        assert producer._compute_mirror_order_volume(\n            True, decimal.Decimal(\"100\"), decimal.Decimal(\"120\"), decimal.Decimal(\"2\"), fees\n        ) == 3\n        # buy order\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), None\n        ) == 2 * (decimal.Decimal(\"100\") / decimal.Decimal(\"80\")) * (1 - producer.max_fees)\n        producer.buy_volume_per_order = 5\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), {}\n        ) == 5\n\n        # with fixed volumes and ignore_exchange_fees\n        producer.ignore_exchange_fees = True\n        assert producer._compute_mirror_order_volume(\n            True, decimal.Decimal(\"100\"), decimal.Decimal(\"120\"), decimal.Decimal(\"2\"), None\n        ) == 3\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), {}\n        ) == 5\n        assert producer._compute_mirror_order_volume(\n            False, decimal.Decimal(\"100\"), decimal.Decimal(\"80\"), decimal.Decimal(\"2\"), fees\n        ) == 5\n\n\nasync def test_create_order():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, consumer, exchange_manager = tools\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer._refresh_symbol_data(symbol_market)\n\n        _origin_decimal_adapt_order_quantity_because_fees = trading_personal_data.decimal_adapt_order_quantity_because_fees\n\n        def _decimal_adapt_order_quantity_because_fees(\n            exchange_manager, symbol: str, order_type: trading_enums.TraderOrderType, quantity: decimal.Decimal,\n            price: decimal.Decimal, side: trading_enums.TradeOrderSide\n        ):\n            return quantity\n\n        with mock.patch.object(\n            trading_personal_data, \"decimal_adapt_order_quantity_because_fees\",\n            mock.Mock(side_effect=_decimal_adapt_order_quantity_because_fees)\n        ) as decimal_adapt_order_quantity_because_fees_mock, mock.patch.object(\n            consumer.trading_mode, \"create_order\",\n            mock.AsyncMock(wraps=consumer.trading_mode.create_order)\n        ) as create_order_mock:\n\n            # SELL\n\n            # enough quantity in portfolio\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(1)\n            side = trading_enums.TradeOrderSide.SELL\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            created_order = (await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies))[0]\n            create_order_mock.assert_called_once()\n            # dependencies are passed to create_order\n            assert create_order_mock.mock_calls[0].kwargs[\"dependencies\"] == dependencies\n            assert created_order.origin_quantity == quantity\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_with(\n                exchange_manager, symbol, trading_enums.TraderOrderType.SELL_LIMIT,\n                created_order.origin_quantity, created_order.origin_price, trading_enums.TradeOrderSide.SELL,\n            )\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n\n            # not enough quantity in portfolio\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(10)\n            side = trading_enums.TradeOrderSide.SELL\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_order = await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies)\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_with(\n                exchange_manager, symbol, trading_enums.TraderOrderType.SELL_LIMIT,\n                decimal.Decimal('10'), decimal.Decimal('100'), trading_enums.TradeOrderSide.SELL\n            )\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert created_order == []\n\n            # just enough quantity in portfolio\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(9)\n            side = trading_enums.TradeOrderSide.SELL\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_order = (await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies))[0]\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert created_order.origin_quantity == quantity\n            assert trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available == decimal.Decimal(0)\n\n            # not enough quantity anymore\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(\"0.0001\")\n            side = trading_enums.TradeOrderSide.SELL\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_orders = await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies)\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available == decimal.Decimal(0)\n            assert created_orders == []\n\n            # enough quantity in portfolio after a small adaptation\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(2.01) # will be adapted\n            side = trading_enums.TradeOrderSide.SELL\n            trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available = decimal.Decimal(\"1.98\")\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_orders = await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies)\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available == decimal.Decimal(\"0.03030001\")\n            assert created_orders[0].origin_quantity == decimal.Decimal(\"1.94969999\")\n\n            # BUY\n\n            # enough quantity in portfolio\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(1)\n            side = trading_enums.TradeOrderSide.BUY\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_order = (await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies))[0]\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_with(\n                exchange_manager, symbol, trading_enums.TraderOrderType.BUY_LIMIT,\n                created_order.origin_quantity, created_order.origin_price, trading_enums.TradeOrderSide.BUY,\n            )\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert created_order.origin_quantity == quantity\n            assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available == 900\n            assert created_order is not None\n\n            # not enough quantity in portfolio\n            price = decimal.Decimal(585)\n            quantity = decimal.Decimal(2)\n            side = trading_enums.TradeOrderSide.BUY\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_orders = await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies)\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_with(\n                exchange_manager, symbol, trading_enums.TraderOrderType.BUY_LIMIT,\n                decimal.Decimal('2'), decimal.Decimal('585'), trading_enums.TradeOrderSide.BUY\n            )\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available == 900\n            assert created_orders == []\n\n            # enough quantity in portfolio\n            price = decimal.Decimal(40)\n            quantity = decimal.Decimal(2)\n            side = trading_enums.TradeOrderSide.BUY\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_order = (await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies))[0]\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert created_order.origin_quantity == quantity\n            assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available == 820\n\n            # enough quantity in portfolio\n            price = decimal.Decimal(205)\n            quantity = decimal.Decimal(4)\n            side = trading_enums.TradeOrderSide.BUY\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_order = (await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies))[0]\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert created_order.origin_quantity == quantity\n            assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available == 0\n            assert created_order is not None\n\n            # not enough quantity in portfolio anymore\n            price = decimal.Decimal(205)\n            quantity = decimal.Decimal(1)\n            side = trading_enums.TradeOrderSide.BUY\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_orders = await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies)\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available == 0\n            assert created_orders == []\n\n            # enough quantity in portfolio after a small adaptation\n            price = decimal.Decimal(100)\n            quantity = decimal.Decimal(1.01) # will be adapted to 1\n            side = trading_enums.TradeOrderSide.BUY\n            trading_api.get_portfolio_currency(exchange_manager, \"USD\").available = decimal.Decimal(\"100\")\n            to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n            created_orders = await consumer.create_order(to_create_order, price, symbol_market, dependencies=dependencies)\n            decimal_adapt_order_quantity_because_fees_mock.assert_called_once()\n            decimal_adapt_order_quantity_because_fees_mock.reset_mock()\n            assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available == decimal.Decimal(\"2.03\")\n            assert created_orders[0].origin_quantity == decimal.Decimal(\"0.97970000\")\n\n\nasync def test_create_state():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, consumer, exchange_manager = tools\n        price = decimal.Decimal(1000)\n        ignore_mirror_orders_only = False\n        ignore_available_funds = False\n        trigger_trailing = False\n        _, _, _, _, producer.symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager, symbol)\n        # not triggering trailing\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], False, dependencies))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing)\n            _create_not_virtual_orders_mock.assert_awaited_once_with([], price, False, dependencies)\n\n        # triggering trailing\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], True, None))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing)\n            _create_not_virtual_orders_mock.assert_awaited_once_with([], price, True, None)\n        trigger_trailing = True\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], True, None))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing)\n            _create_not_virtual_orders_mock.assert_awaited_once_with([], price, True, None)\n\n        # already trailing: skip call\n        producer.is_currently_trailing = True\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], True, None))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_not_called()\n            _create_not_virtual_orders_mock.assert_not_called()\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], False, None))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_not_called()\n            _create_not_virtual_orders_mock.assert_not_called()\n\n        # not tailing anymore: can now call\n        producer.is_currently_trailing = False\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], True, None))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing)\n            _create_not_virtual_orders_mock.assert_awaited_once_with([], price, True, None)\n        trigger_trailing = True\n        with mock.patch.object(producer, \"_generate_staggered_orders\", mock.AsyncMock(return_value=([], [], False, None))) \\\n            as _generate_staggered_orders_mock, mock.patch.object(producer, \"_create_not_virtual_orders\", mock.AsyncMock()) \\\n            as _create_not_virtual_orders_mock:\n            await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing)\n            _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing)\n            _create_not_virtual_orders_mock.assert_awaited_once_with([], price, False, None)\n\n\nasync def test_create_new_orders():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, consumer, exchange_manager = tools\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=producer.symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer._refresh_symbol_data(symbol_market)\n\n        # valid input\n        price = decimal.Decimal(205)\n        quantity = decimal.Decimal(1)\n        side = trading_enums.TradeOrderSide.BUY\n        to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False)\n        producer.is_currently_trailing = True\n        data = {\n            consumer.ORDER_DATA_KEY: to_create_order,\n            consumer.CURRENT_PRICE_KEY: price,\n            consumer.SYMBOL_MARKET_KEY: symbol_market,\n            consumer.COMPLETING_TRAILING_KEY: False,\n        }\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        assert await consumer.create_new_orders(symbol, None, None, data=data, dependencies=dependencies)\n        assert producer.is_currently_trailing is True\n        data = {\n            consumer.ORDER_DATA_KEY: to_create_order,\n            consumer.CURRENT_PRICE_KEY: price,\n            consumer.SYMBOL_MARKET_KEY: symbol_market,\n            consumer.COMPLETING_TRAILING_KEY: True, # will update producer.is_currently_trailing\n        }\n        assert await consumer.create_new_orders(symbol, None, None, data=data, dependencies=dependencies)\n        assert producer.is_currently_trailing is False  # updated to false\n\n        # invalid input 1\n        data = {\n            consumer.ORDER_DATA_KEY: to_create_order,\n            consumer.CURRENT_PRICE_KEY: price\n        }\n        with pytest.raises(KeyError):\n            await consumer.create_new_orders(symbol, None, None, data=data, dependencies=None)\n\n        # invalid input 2\n        data = {}\n        with pytest.raises(KeyError):\n            await consumer.create_new_orders(symbol, None, None, data=data)\n\n        # invalid input 3\n        with pytest.raises(KeyError):\n            await consumer.create_new_orders(symbol, None, None)\n\n\nasync def test_ensure_current_price_in_limit_parameters():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.already_errored_on_out_of_window_price = False\n\n        with mock.patch.object(producer, \"_log_window_error_or_warning\", mock.Mock()) \\\n                as _log_window_error_or_warning_mock:\n            # price too low (lower bound is 1)\n            producer._ensure_current_price_in_limit_parameters(0.1)\n            _log_window_error_or_warning_mock.assert_called_once()\n            assert _log_window_error_or_warning_mock.mock_calls[0].args[1] is True\n            _log_window_error_or_warning_mock.reset_mock()\n            assert producer.already_errored_on_out_of_window_price is True\n            producer._ensure_current_price_in_limit_parameters(0.1)\n            assert _log_window_error_or_warning_mock.mock_calls[0].args[1] is False\n            _log_window_error_or_warning_mock.reset_mock()\n            assert producer.already_errored_on_out_of_window_price is True\n\n            producer.already_errored_on_out_of_window_price = False\n            # price too high (higher bound is 10000)\n            producer._ensure_current_price_in_limit_parameters(999999)\n            assert _log_window_error_or_warning_mock.mock_calls[0].args[1] is True\n            _log_window_error_or_warning_mock.reset_mock()\n            assert producer.already_errored_on_out_of_window_price is True\n            producer._ensure_current_price_in_limit_parameters(999999)\n            assert _log_window_error_or_warning_mock.mock_calls[0].args[1] is False\n            _log_window_error_or_warning_mock.reset_mock()\n            assert producer.already_errored_on_out_of_window_price is True\n\n\nasync def test_single_exchange_process_optimize_initial_portfolio():\n    async with _get_tools(\"BTC/USD\") as tools:\n        producer, _, exchange_manager = tools\n        mode = producer.trading_mode\n        exchange_manager.exchange_config.traded_symbol_pairs = [\"BTC/USD\"]\n        exchange_manager.client_symbols = [\"BTC/USD\"]\n\n        initial_portfolio = exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n        assert initial_portfolio[\"BTC\"].available == decimal.Decimal(\"10\")\n        assert initial_portfolio[\"USD\"].available == decimal.Decimal(\"1000\")\n\n        limit_buy = trading_personal_data.BuyLimitOrder(exchange_manager.trader)\n        limit_buy.update(order_type=trading_enums.TraderOrderType.BUY_LIMIT,\n                         symbol=\"BTC/USD\",\n                         current_price=decimal.Decimal(str(50)),\n                         quantity=decimal.Decimal(str(2)),\n                         price=decimal.Decimal(str(50)))\n        await exchange_manager.exchange_personal_data.orders_manager.upsert_order_instance(limit_buy)\n\n        orders = await mode.single_exchange_process_optimize_initial_portfolio(\n            [\"BTC\", \"ETH\"], \"USD\", {\"BTC/USD\": {trading_enums.ExchangeConstantsTickersColumns.CLOSE.value: 1000}}\n        )\n        cancelled_orders, part_1_orders, part_2_orders = [orders[0], orders[1], orders[2]]\n\n        assert len(cancelled_orders) == 1\n        assert cancelled_orders[0] is limit_buy\n\n        assert len(part_1_orders) == 1\n        part_1_order = part_1_orders[0]\n        assert isinstance(part_1_order, trading_personal_data.SellMarketOrder)\n        assert part_1_order.created_last_price == decimal.Decimal(\"1000\")\n        assert part_1_order.origin_quantity == decimal.Decimal(\"10\")    # 10 BTC to sell into 10 000 USD\n        assert part_1_order.status == trading_enums.OrderStatus.FILLED\n\n        assert part_2_orders\n        part_2_order = part_2_orders[0]\n        assert isinstance(part_2_order, trading_personal_data.BuyMarketOrder)\n        assert part_2_order.created_last_price == decimal.Decimal(\"1000\")\n        assert part_2_order.origin_quantity == decimal.Decimal(\"5.545\")    # 50% of funds\n        assert part_2_order.status == trading_enums.OrderStatus.FILLED\n\n        # check portfolio is rebalanced\n        final_portfolio = exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n        assert final_portfolio[\"BTC\"].available == decimal.Decimal('5.539455')  # 5.545 - fees\n        assert final_portfolio[\"USD\"].available == decimal.Decimal(\"5545\")\n\n\nasync def test_prepare_trailing():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        with mock.patch.object(\n            producer, \"_prepare_order_by_order_trailing\", mock.AsyncMock(\n                return_value=([\"_prepare_order_by_order_trailing\"], [], [], [], None)\n            )\n        ) as _prepare_order_by_order_trailing_mock, mock.patch.object(\n            producer, \"_prepare_full_grid_trailing\", mock.AsyncMock(\n                return_value=([\"_prepare_full_grid_trailing\"], [], [], [], None\n            ))\n        ) as _prepare_full_grid_trailing_mock:\n            sorted_orders = [mock.Mock(side=trading_enums.TradeOrderSide.BUY)]\n            recently_closed_trades = [\"trades\"]\n            lowest_buy = decimal.Decimal(1)\n            highest_buy = decimal.Decimal(2)\n            lowest_sell = decimal.Decimal(3)\n            highest_sell = decimal.Decimal(4)\n            dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            log_header = \"[binance] BTC/USD @ 1.5 full grid trailing up process: \"\n\n            # current_price can't be <= 0\n            for _current_price in [-1.5, 0]:\n                current_price = decimal.Decimal(_current_price)\n                assert await producer._prepare_trailing(sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, dependencies) == (\n                    [], [], [], [], None\n                )\n                _prepare_order_by_order_trailing_mock.assert_not_called()\n                _prepare_full_grid_trailing_mock.assert_not_called()\n\n\n            current_price = decimal.Decimal(1.5)\n            producer.use_order_by_order_trailing = False\n            assert await producer._prepare_trailing(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, dependencies\n            ) == (\n                [\"_prepare_full_grid_trailing\"], [], [], [], None\n            )\n            _prepare_order_by_order_trailing_mock.assert_not_called()\n            _prepare_full_grid_trailing_mock.assert_awaited_once_with(\n                sorted_orders, current_price, dependencies, log_header\n            )\n            _prepare_full_grid_trailing_mock.reset_mock()\n\n\n            sorted_orders = [mock.Mock(side=trading_enums.TradeOrderSide.SELL)]\n            log_header = \"[binance] BTC/USD @ 1.5 order by order trailing down process: \"\n            producer.use_order_by_order_trailing = True\n            assert await producer._prepare_trailing(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, dependencies\n            ) == (\n                [\"_prepare_order_by_order_trailing\"], [], [], [], None\n            )\n            _prepare_full_grid_trailing_mock.assert_not_called()\n            _prepare_order_by_order_trailing_mock.assert_awaited_once_with(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, False, dependencies, log_header\n            )\n\n\nasync def test_prepare_order_by_order_trailing():\n    symbol = \"BTC/USD\"\n    sorted_orders = [mock.Mock(order_id=\"123\", origin_price=decimal.Decimal(str(50)))]\n    recently_closed_trades = [\"trades\"]\n    lowest_buy = decimal.Decimal(1)\n    highest_buy = decimal.Decimal(2)\n    lowest_sell = decimal.Decimal(3)\n    highest_sell = decimal.Decimal(4)\n    dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n    log_header = \"[binance] BTC/USD @ 25 full grid trailing process: \"\n    current_price = decimal.Decimal(25)\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        replaced_buy_orders = [\n            staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.BUY, decimal.Decimal(str(price)), decimal.Decimal(str(amount)), symbol, False\n            )\n            for price, amount in [\n                (10, 0.02),\n                (15, 0.017),\n                (20, 0.015),\n            ]\n        ]\n        replaced_sell_orders = [\n            staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.SELL, decimal.Decimal(str(price)), decimal.Decimal(str(amount)), symbol, False\n            )\n            for price, amount in [\n                (30, 0.013),\n                (40, 0.01),\n            ]\n        ]\n        to_cancel_orders_with_trailed_prices = [\n            (sorted_orders[0], decimal.Decimal(50)),\n        ]\n        to_execute_order_with_trailing_price = (replaced_buy_orders[0], decimal.Decimal(60))\n        convert_order = mock.Mock(order_id=\"123\")\n        trailing_buy_orders = [123] \n        trailing_sell_orders = [456]\n        cancelled_replaced_orders = [] \n        cancelled_orders = [sorted_orders[0]]\n        is_trailing_up = True\n        with mock.patch.object(\n            producer, \"_compute_trailing_replaced_orders\", mock.AsyncMock(\n                return_value=(replaced_buy_orders, replaced_sell_orders)\n            )\n        ) as _compute_trailing_replaced_orders_mock, mock.patch.object(\n            producer, \"_get_orders_to_replace_with_updated_price_for_trailing\", mock.Mock(\n                return_value=(to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price)\n            )\n        ) as _get_orders_to_replace_with_updated_price_for_trailing_mock, mock.patch.object(\n            producer, \"_cancel_replaced_orders\", mock.AsyncMock(\n                return_value=(cancelled_replaced_orders, cancelled_orders, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")]))\n            )\n        ) as _cancel_replaced_orders_mock, mock.patch.object(\n            producer, \"_convert_order_funds\", mock.AsyncMock(\n                return_value=[convert_order]\n            )\n        ) as _convert_order_funds_mock, mock.patch.object(\n            producer, \"_get_updated_trailing_orders\", mock.Mock(\n                return_value=(trailing_buy_orders, trailing_sell_orders)\n            )\n        ) as _get_updated_trailing_orders, mock.patch.object(\n            producer, \"_prepare_full_grid_trailing\", mock.AsyncMock(\n                return_value=([\"9\"], [\"123\"], [\"456\"], [\"789\"], \"plop\")\n            )\n        ) as _prepare_full_grid_trailing_orders:\n            assert await producer._prepare_order_by_order_trailing(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, is_trailing_up, dependencies, log_header\n            ) == (\n                [sorted_orders[0]], [convert_order], trailing_buy_orders, trailing_sell_orders,\n                trading_signals.get_orders_dependencies([convert_order])\n            )\n            _compute_trailing_replaced_orders_mock.assert_awaited_once_with(\n                sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, True, log_header\n            )\n            _get_orders_to_replace_with_updated_price_for_trailing_mock.assert_called_once_with(\n                sorted_orders, replaced_buy_orders+replaced_sell_orders, current_price\n            )\n            _cancel_replaced_orders_mock.assert_awaited_once_with([o[0] for o in to_cancel_orders_with_trailed_prices + [to_execute_order_with_trailing_price]], dependencies)\n            _convert_order_funds_mock.assert_awaited_once_with(\n                to_execute_order_with_trailing_price[0], current_price, dependencies, log_header\n            )\n            _get_updated_trailing_orders.assert_called_once_with(\n                replaced_buy_orders, replaced_sell_orders, cancelled_replaced_orders, to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price, current_price, is_trailing_up\n            )\n            _prepare_full_grid_trailing_orders.assert_not_called()\n            _compute_trailing_replaced_orders_mock.reset_mock()\n            _get_orders_to_replace_with_updated_price_for_trailing_mock.reset_mock()\n            _cancel_replaced_orders_mock.reset_mock()\n            _convert_order_funds_mock.reset_mock()\n            _get_updated_trailing_orders.reset_mock()\n\n            # with _get_orders_to_replace_with_updated_price_for_trailing raising an error\n            with mock.patch.object(\n                producer, \"_get_orders_to_replace_with_updated_price_for_trailing\", mock.Mock(\n                    side_effect=ValueError(\"test\")\n                )\n            ) as _get_orders_to_replace_with_updated_price_for_trailing_mock:\n                assert await producer._prepare_order_by_order_trailing(\n                    sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, is_trailing_up, dependencies, log_header\n                ) == (\n                    [], [], [], [], None\n                )\n                _compute_trailing_replaced_orders_mock.assert_awaited_once_with(\n                    sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, True, log_header\n                )\n                _get_orders_to_replace_with_updated_price_for_trailing_mock.assert_called_once_with(\n                    sorted_orders, replaced_buy_orders+replaced_sell_orders, current_price\n                )\n                _prepare_full_grid_trailing_orders.assert_not_called()\n                _compute_trailing_replaced_orders_mock.reset_mock()\n                _get_orders_to_replace_with_updated_price_for_trailing_mock.reset_mock()\n                _cancel_replaced_orders_mock.assert_not_called()\n                _convert_order_funds_mock.assert_not_called()\n                _get_updated_trailing_orders.assert_not_called()\n            with mock.patch.object(\n                producer, \"_get_orders_to_replace_with_updated_price_for_trailing\", mock.Mock(\n                    side_effect=staggered_orders_trading.TrailingAborted(\"test\")\n                )\n            ) as _get_orders_to_replace_with_updated_price_for_trailing_mock:\n                assert await producer._prepare_order_by_order_trailing(\n                    sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, is_trailing_up, dependencies, log_header\n                ) == (\n                    [], [], replaced_buy_orders, replaced_sell_orders, None\n                )\n                _compute_trailing_replaced_orders_mock.assert_awaited_once_with(\n                    sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, True, log_header\n                )\n                _get_orders_to_replace_with_updated_price_for_trailing_mock.assert_called_once_with(\n                    sorted_orders, replaced_buy_orders+replaced_sell_orders, current_price\n                )\n                _prepare_full_grid_trailing_orders.assert_not_called()\n                _compute_trailing_replaced_orders_mock.reset_mock()\n                _get_orders_to_replace_with_updated_price_for_trailing_mock.reset_mock()\n                _cancel_replaced_orders_mock.assert_not_called()\n                _convert_order_funds_mock.assert_not_called()\n                _get_updated_trailing_orders.assert_not_called()\n            with mock.patch.object(\n                producer, \"_get_orders_to_replace_with_updated_price_for_trailing\", mock.Mock(\n                    side_effect=staggered_orders_trading.NoOrdersToTrail(\"test\")\n                )\n            ) as _get_orders_to_replace_with_updated_price_for_trailing_mock:\n                assert await producer._prepare_order_by_order_trailing(\n                    sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, is_trailing_up, dependencies, log_header\n                ) == (\n                    [\"9\"], [\"123\"], [\"456\"], [\"789\"], \"plop\"\n                )\n                _compute_trailing_replaced_orders_mock.assert_awaited_once_with(\n                    sorted_orders, recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, True, log_header\n                )\n                _get_orders_to_replace_with_updated_price_for_trailing_mock.assert_called_once_with(\n                    sorted_orders, replaced_buy_orders+replaced_sell_orders, current_price\n                )\n                _prepare_full_grid_trailing_orders.assert_awaited_once_with(\n                    sorted_orders, current_price, dependencies, log_header\n                )\n                _prepare_full_grid_trailing_orders.reset_mock()\n                _compute_trailing_replaced_orders_mock.reset_mock()\n                _get_orders_to_replace_with_updated_price_for_trailing_mock.reset_mock()\n                _cancel_replaced_orders_mock.assert_not_called()\n                _convert_order_funds_mock.assert_not_called()\n                _get_updated_trailing_orders.assert_not_called()\n\n\nasync def test_compute_trailing_replaced_orders():\n    symbol = \"BTC/USD\"\n    recently_closed_trades = [\"trades\"]\n    current_price = decimal.Decimal(400)\n    lowest_buy = decimal.Decimal(1)\n    highest_buy = current_price\n    lowest_sell = current_price\n    highest_sell = decimal.Decimal(10000)\n    ignore_available_funds = \"ignore_available_funds\"\n    log_header = \"[binance] BTC/USD @ 25 order by order trailing process: \"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n\n        with mock.patch.object(\n            producer, \"_handle_missed_mirror_orders_fills\", mock.AsyncMock()\n        ) as _handle_missed_mirror_orders_fills_mock:\n            # no orders, raises NoOrdersToTrail\n            with pytest.raises(staggered_orders_trading.NoOrdersToTrail):\n                await producer._compute_trailing_replaced_orders(\n                    [], recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds, log_header\n                )\n            _handle_missed_mirror_orders_fills_mock.assert_not_called()\n            with mock.patch.object(\n                producer, \"_analyse_current_orders_situation\", mock.Mock(return_value=([], staggered_orders_trading.StaggeredOrdersTradingModeProducer.ERROR, None))\n            ) as _analyse_current_orders_situation_mock:\n                with pytest.raises(ValueError):\n                    await producer._compute_trailing_replaced_orders(\n                        [], recently_closed_trades, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds, log_header\n                    )\n                _analyse_current_orders_situation_mock.reset_mock()\n            _handle_missed_mirror_orders_fills_mock.assert_not_called()\n            trading_api.force_set_mark_price(exchange_manager, producer.symbol, current_price)\n\n            await producer._ensure_staggered_orders()\n            await asyncio.create_task(_wait_for_orders_creation(producer.operational_depth))\n            open_orders = trading_api.get_open_orders(exchange_manager)\n            assert len(open_orders) == producer.operational_depth\n            sorted_orders = sorted(open_orders, key=lambda order: order.origin_price)\n            highest_sell = sorted_orders[-1].origin_price\n            \n            # nothing to replace\n            replaced_buy_orders, replaced_sell_orders = await producer._compute_trailing_replaced_orders(\n                sorted_orders, [], lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds, log_header\n            )\n            assert replaced_buy_orders == replaced_sell_orders == []\n            _handle_missed_mirror_orders_fills_mock.assert_not_called()\n\n            # with missing orders: returned as replaced orders\n            input_open_orders = sorted_orders[:23] + sorted_orders[26:]\n            # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS\n            staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None)\n            replaced_buy_orders, replaced_sell_orders = await producer._compute_trailing_replaced_orders(\n                input_open_orders, [], lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds, log_header\n            )\n            assert replaced_buy_orders == [\n                staggered_orders_trading.OrderData(\n                    trading_enums.TradeOrderSide.BUY, decimal.Decimal(str(amount)), decimal.Decimal(str(price)), symbol, False\n                )\n                for price, amount in [\n                    (372, \"0.0583333333333333370\"),\n                    (388, \"0.0541666666666666630\"),\n                ]\n            ] # 2 buy orders missing\n            assert replaced_sell_orders == [\n                staggered_orders_trading.OrderData(\n                    trading_enums.TradeOrderSide.SELL, decimal.Decimal(str(amount)), decimal.Decimal(str(price)), symbol, False\n                )\n                for price, amount in [\n                    (412, \"0.2166666666666666520\"),\n                ]\n            ] # 1 sell order missing\n            missing_orders = [(decimal.Decimal(str(price)), trading_enums.TradeOrderSide.BUY) for price in [372, 388]] + [(decimal.Decimal(str(price)), trading_enums.TradeOrderSide.SELL) for price in [412]]\n            _handle_missed_mirror_orders_fills_mock.assert_awaited_once_with(\n                [], missing_orders, current_price\n            )\n\n\nasync def test_cancel_replaced_orders():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        with mock.patch.object(\n            producer, \"_cancel_open_order\", mock.AsyncMock(return_value=(True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"345\")])))\n        ) as _cancel_open_order_mock:\n            # 1. no replaced orders\n            cancelled_replaced_orders, cancelled_orders, dependencies = await producer._cancel_replaced_orders(\n                [], trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            )\n            _cancel_open_order_mock.assert_not_called()\n            assert cancelled_replaced_orders == []\n            assert cancelled_orders == []\n            assert dependencies == commons_signals.SignalDependencies()\n\n            # 2. replaced \"real\" orders\n            replaced_orders = [mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"345\")]\n            cancelled_replaced_orders, cancelled_orders, dependencies = await producer._cancel_replaced_orders(\n                replaced_orders, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            )\n            _cancel_open_order_mock.assert_has_calls([\n                mock.call(replaced_orders[0], trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])),\n                mock.call(replaced_orders[1], trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])),\n            ])\n            assert cancelled_replaced_orders == []\n            assert cancelled_orders == replaced_orders\n            assert dependencies == trading_signals.get_orders_dependencies([mock.Mock(order_id=\"345\"), mock.Mock(order_id=\"345\")])\n\n            # 3. replaced \"real\" orders and \"fake\" orders\n            replaced_orders = [mock.Mock(order_id=\"123\"), staggered_orders_trading.OrderData(trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.01\"), decimal.Decimal(\"100\"), symbol, False)]\n            cancelled_replaced_orders, cancelled_orders, dependencies = await producer._cancel_replaced_orders(\n                replaced_orders, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n            )\n            _cancel_open_order_mock.assert_has_calls([\n                mock.call(replaced_orders[0], trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])),\n            ])\n            assert cancelled_replaced_orders == [replaced_orders[1]]\n            assert cancelled_orders == [replaced_orders[0]]\n            assert dependencies == trading_signals.get_orders_dependencies([mock.Mock(order_id=\"345\")])\n\n\nasync def test_convert_order_funds():\n    symbol = \"BTC/USD\"\n    log_header = \"[binance] BTC/USD @ 25 convert order funds process: \"\n    convert_dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n    quantity = decimal.Decimal(\"0.01\")\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        current_price = decimal.Decimal(25)\n        \n        # Test 1: BUY order with trading_personal_data.Order\n        buy_price = decimal.Decimal(10)\n        to_convert_buy_order = trading_personal_data.create_order_instance(\n            exchange_manager.trader, trading_enums.TraderOrderType.BUY_LIMIT, symbol, current_price, quantity, price=buy_price\n        )\n        convert_orders = await producer._convert_order_funds(\n            to_convert_buy_order, current_price, convert_dependencies, log_header\n        )\n        assert len(convert_orders) == 1\n        assert isinstance(convert_orders[0], trading_personal_data.BuyMarketOrder)\n        assert convert_orders[0].symbol == \"BTC/USD\"\n        assert convert_orders[0].origin_quantity == decimal.Decimal(\"0.004\") # 0.01 * 10 / 25 (buy order quantity x price / current price)\n        assert convert_orders[0].origin_price == decimal.Decimal(\"25\")\n        assert convert_orders[0].total_cost == decimal.Decimal(\"0.1\") # 0.01 * 10 (buy order quantity x price)\n        assert convert_orders[0].status == trading_enums.OrderStatus.FILLED\n        \n        # Test 2: SELL order with trading_personal_data.Order\n        sell_price = decimal.Decimal(30)\n        to_convert_sell_order = trading_personal_data.create_order_instance(\n            exchange_manager.trader, trading_enums.TraderOrderType.SELL_LIMIT, symbol, current_price, quantity, price=sell_price\n        )\n        convert_orders = await producer._convert_order_funds(\n            to_convert_sell_order, current_price, convert_dependencies, log_header\n        )\n        assert len(convert_orders) == 1\n        assert isinstance(convert_orders[0], trading_personal_data.SellMarketOrder)\n        assert convert_orders[0].symbol == \"BTC/USD\"\n        assert convert_orders[0].origin_quantity == decimal.Decimal(\"0.01\")\n        assert convert_orders[0].origin_price == decimal.Decimal(\"25\")\n        assert convert_orders[0].total_cost == decimal.Decimal(\"0.25\") # sell order quantity\n        assert convert_orders[0].status == trading_enums.OrderStatus.FILLED\n        \n        # Test 3: BUY order with OrderData\n        buy_price = decimal.Decimal(15)\n        to_convert_buy_order_data = staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.BUY, quantity, buy_price, symbol, False\n        )\n        convert_orders = await producer._convert_order_funds(\n            to_convert_buy_order_data, current_price, convert_dependencies, log_header\n        )\n        assert len(convert_orders) == 1\n        assert isinstance(convert_orders[0], trading_personal_data.BuyMarketOrder)\n        assert convert_orders[0].symbol == \"BTC/USD\"\n        assert convert_orders[0].origin_quantity == decimal.Decimal(\"0.006\") # 0.01 * 10 / 15 (buy order quantity x price / current price)\n        assert convert_orders[0].origin_price == decimal.Decimal(\"25\")\n        assert convert_orders[0].status == trading_enums.OrderStatus.FILLED\n        \n        # Test 4: SELL order with OrderData\n        sell_price = decimal.Decimal(35)\n        to_convert_sell_order_data = staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.SELL, quantity, sell_price, symbol, False\n        )\n        convert_orders = await producer._convert_order_funds(\n            to_convert_sell_order_data, current_price, convert_dependencies, log_header\n        )\n        assert len(convert_orders) == 1\n        # Verify the convert order properties\n        convert_order = convert_orders[0]\n        assert isinstance(convert_orders[0], trading_personal_data.SellMarketOrder)\n        assert convert_orders[0].symbol == \"BTC/USD\"\n        assert convert_orders[0].origin_quantity == decimal.Decimal(\"0.01\")\n        assert convert_orders[0].origin_price == decimal.Decimal(\"25\")\n        assert convert_orders[0].status == trading_enums.OrderStatus.FILLED\n\n\nasync def test_get_updated_trailing_orders():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        current_price = decimal.Decimal(\"25\")\n        \n        # Test 1: to_convert_order with sufficient funds, trailing up\n        to_convert_order = trading_personal_data.create_order_instance(\n            exchange_manager.trader, trading_enums.TraderOrderType.BUY_LIMIT, symbol, current_price, \n            decimal.Decimal(\"0.01\"), price=decimal.Decimal(\"20\")\n        )\n        \n        replaced_buy_orders = [\n            staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.02\"), decimal.Decimal(\"15\"), symbol, False\n            )\n        ]\n        replaced_sell_orders = [\n            staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.03\"), decimal.Decimal(\"30\"), symbol, False\n            )\n        ]\n        cancelled_replaced_orders = []\n        to_cancel_orders_with_trailed_prices = [\n            (replaced_buy_orders[0], decimal.Decimal(\"18\")),\n            (replaced_sell_orders[0], decimal.Decimal(\"28\"))\n        ]\n        to_execute_order_with_trailing_price = (to_convert_order, decimal.Decimal(\"22\"))\n        \n        trailing_buy_orders, trailing_sell_orders = producer._get_updated_trailing_orders(\n            replaced_buy_orders, replaced_sell_orders, cancelled_replaced_orders,\n            to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price, current_price, True\n        )\n        \n        # Verify results\n        assert len(trailing_buy_orders) == 2  # 1 from replaced + 1 from to_cancel_orders_with_trailed_prices\n        assert len(trailing_sell_orders) == 3  # 1 from replaced + 1 from to_cancel_orders_with_trailed_prices + 1 from to_convert_order\n\n        assert trailing_buy_orders[0] is replaced_buy_orders[0]\n        assert trailing_buy_orders[1] == staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.01666666666666666666666666667\"), decimal.Decimal(\"18\"), symbol, False\n        )\n        assert trailing_sell_orders[0] is replaced_sell_orders[0]\n        assert trailing_sell_orders[1] == staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.03\"), decimal.Decimal(\"28\"), symbol, False\n        )\n        assert trailing_sell_orders[2] == staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.009090909090909090909090909091\"), decimal.Decimal(\"22\"), symbol, False\n        )\n        assert trailing_sell_orders[2].quantity * trailing_sell_orders[2].price == to_convert_order.origin_price * to_convert_order.origin_quantity\n\n        # Test 2: to_convert_order with sufficient funds and cancelled orders, trailing down\n        to_convert_order = trading_personal_data.create_order_instance(\n            exchange_manager.trader, trading_enums.TraderOrderType.SELL_LIMIT, symbol, current_price, \n            decimal.Decimal(\"0.033\"), price=decimal.Decimal(\"35\")\n        )\n        \n        replaced_buy_orders = [\n            staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.02\"), decimal.Decimal(\"15\"), symbol, False\n            ),\n            staggered_orders_trading.OrderData( # cancelled\n                trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.02\"), decimal.Decimal(\"16\"), symbol, False\n            ),\n        ]\n        replaced_sell_orders = [\n            staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.03\"), decimal.Decimal(\"30\"), symbol, False\n            ),\n            staggered_orders_trading.OrderData( # cancelled\n                trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.03\"), decimal.Decimal(\"31\"), symbol, False\n            ),\n        ]\n        cancelled_replaced_orders = [replaced_buy_orders[1], replaced_sell_orders[1]]\n        to_cancel_orders_with_trailed_prices = [\n            (replaced_buy_orders[0], decimal.Decimal(\"18\")),\n            (replaced_sell_orders[0], decimal.Decimal(\"28\"))\n        ]\n        current_price = decimal.Decimal(\"22\") # also ensure current price == convert order price is supported\n        to_execute_order_with_trailing_price = (to_convert_order, decimal.Decimal(\"22\"))\n        \n        trailing_buy_orders, trailing_sell_orders = producer._get_updated_trailing_orders(\n            replaced_buy_orders, replaced_sell_orders, cancelled_replaced_orders,\n            to_cancel_orders_with_trailed_prices, to_execute_order_with_trailing_price, current_price, False\n        )\n        \n        # Verify results\n        assert len(trailing_buy_orders) == 3  # 1 from replaced + 1 from to_cancel_orders_with_trailed_prices + 1 from to_convert_order\n        assert len(trailing_sell_orders) == 2  # 1 from replaced + 1 from to_cancel_orders_with_trailed_prices\n\n        assert trailing_buy_orders[0] is replaced_buy_orders[0]\n        assert trailing_buy_orders[1] == staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.01666666666666666666666666667\"), decimal.Decimal(\"18\"), symbol, False\n        )\n        assert trailing_buy_orders[2] == staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.0525\"), decimal.Decimal(\"22\"), symbol, False\n        )\n        assert trailing_buy_orders[2].quantity * trailing_buy_orders[2].price == to_convert_order.origin_price * to_convert_order.origin_quantity\n        assert trailing_sell_orders[0] is replaced_sell_orders[0]\n        assert trailing_sell_orders[1] == staggered_orders_trading.OrderData(\n            trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.03\"), decimal.Decimal(\"28\"), symbol, False\n        )\n\n        # Test 3: to_convert_order with insufficient funds, trailing down\n        to_convert_order_2 = trading_personal_data.create_order_instance(\n            exchange_manager.trader, trading_enums.TraderOrderType.SELL_LIMIT, symbol, current_price, \n            decimal.Decimal(\"0.1\"), price=decimal.Decimal(\"30\")\n        )\n        \n        # Mock insufficient funds scenario\n        with mock.patch.object(trading_api, 'get_portfolio_currency') as mock_portfolio:\n            mock_portfolio.return_value.available = decimal.Decimal(\"0.05\")  # Less than required 0.1\n            \n            trailing_buy_orders_2, trailing_sell_orders_2 = producer._get_updated_trailing_orders(\n                [], [], [],\n                [], (to_convert_order_2, decimal.Decimal(\"28\")), current_price, False\n            )\n            \n            # Should use available amount instead of ideal quantity\n            assert len(trailing_buy_orders_2) == 1\n            assert len(trailing_sell_orders_2) == 0\n            assert trailing_buy_orders_2[0] == staggered_orders_trading.OrderData(\n                trading_enums.TradeOrderSide.BUY, decimal.Decimal(\"0.001785714285714285714285714286\"), decimal.Decimal(\"28\"), symbol, False\n            )\n        \n        # Test 4: Regular cancelled order with trading_personal_data.Order\n        regular_trading_order = trading_personal_data.create_order_instance(\n            exchange_manager.trader, trading_enums.TraderOrderType.SELL_LIMIT, symbol, current_price, \n            decimal.Decimal(\"0.08\"), price=decimal.Decimal(\"32\")\n        )\n        \n        trailing_buy_orders_4, trailing_sell_orders_4 = producer._get_updated_trailing_orders(\n            [], [], [],\n            [(regular_trading_order, decimal.Decimal(\"30\"))], (to_convert_order, decimal.Decimal(\"22\")), current_price, True\n        )\n        \n        assert trailing_buy_orders_4 == []\n        # Regular SELL order should keep same quantity\n        assert len(trailing_sell_orders_4) == 2\n        assert trailing_sell_orders_4 == [\n            staggered_orders_trading.OrderData(\n                # same as original\n                trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.08\"), decimal.Decimal(\"30\"), symbol, False\n            ),\n            staggered_orders_trading.OrderData(\n                # converted order\n                trading_enums.TradeOrderSide.SELL, decimal.Decimal(\"0.0525\"), decimal.Decimal(\"22\"), symbol, False\n            ),\n        ]\n        assert trailing_sell_orders_4[1].quantity * trailing_sell_orders_4[1].price == to_convert_order.origin_price * to_convert_order.origin_quantity\n\n\nasync def test_get_orders_to_replace_with_updated_price_for_trailing_up():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.flat_increment = decimal.Decimal(\"5\")\n        producer.flat_spread = decimal.Decimal(\"10\")\n        current_price = decimal.Decimal(\"110.1\") # will create buy orders at 100, 105 and a sell order at 115\n        side = trading_enums.TradeOrderSide.BUY\n        quantity = decimal.Decimal(\"0.01\")\n        # 0. no input orders: should not happen, raises\n        with pytest.raises(ValueError):\n            producer._get_orders_to_replace_with_updated_price_for_trailing(\n                [], [], current_price\n            )\n\n        sorted_orders = [\n            trading_personal_data.create_order_instance(\n                exchange_manager.trader, trading_enums.TraderOrderType.BUY_LIMIT, symbol, current_price, quantity, price=decimal.Decimal(str(price))\n            )\n            for price in range (10, 100, 5) # 10 to 95\n        ]\n        # 1. no replaced orders, only existing orders, 4 orders to replace\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, [], current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            (sorted_orders[1], decimal.Decimal(str(100))),\n            (sorted_orders[2], decimal.Decimal(str(105))),\n        ]\n        assert order_to_replace_by_other_side_order == sorted_orders[0]\n        assert other_side_order_price == decimal.Decimal(str(115))\n\n        # 1. replaced orders and existing orders, same result: 4 orders to replace\n        sorted_orders = [\n            trading_personal_data.create_order_instance(\n                exchange_manager.trader, trading_enums.TraderOrderType.BUY_LIMIT, symbol, current_price, quantity, price=decimal.Decimal(str(price))\n            )\n            for price in range (10, 90, 5) # 10 to 85\n        ]\n        replaced_orders = [\n            staggered_orders_trading.OrderData(side, quantity, decimal.Decimal(str(price)), symbol, False)\n            for price in range(90, 100, 5) # 90 to 95\n        ]\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            (sorted_orders[1], decimal.Decimal(str(100))),\n            (sorted_orders[2], decimal.Decimal(str(105))),\n        ]\n        assert order_to_replace_by_other_side_order == sorted_orders[0]\n        assert other_side_order_price == decimal.Decimal(str(115))\n\n        # 2. replaced orders and existing orders, only 1 order to replace\n        for current_price in [decimal.Decimal(\"95.1\"), decimal.Decimal(\"100\"), decimal.Decimal(\"104.9\"), decimal.Decimal(\"105\")]:\n            orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n                sorted_orders, replaced_orders, current_price\n            )\n            assert orders_to_replace_with_trailed_price == []\n            # only the other side order is set\n            assert order_to_replace_by_other_side_order == sorted_orders[0]\n            assert other_side_order_price == decimal.Decimal(str(105))\n\n        # 3. replaced orders and existing orders, only 2 orders to replace\n        for current_price in [decimal.Decimal(\"105.1\"), decimal.Decimal(\"109.9\"), decimal.Decimal(\"110\")]:\n            orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n                sorted_orders, replaced_orders, current_price\n            )\n            assert orders_to_replace_with_trailed_price == [\n                (sorted_orders[1], decimal.Decimal(str(100))),\n            ]\n            # only the other side order is set\n            assert order_to_replace_by_other_side_order == sorted_orders[0]\n            assert other_side_order_price == decimal.Decimal(str(110))\n\n        # all sorted_orders to replace, but not replaced_orders\n        current_price = decimal.Decimal(\"176\")\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            (sorted_orders[i], decimal.Decimal(str(95 + i * 5)))\n            for i in range(1, len(sorted_orders))\n        ]\n        # only the other side order is set\n        assert order_to_replace_by_other_side_order == sorted_orders[0]\n        assert other_side_order_price == decimal.Decimal(str(180))\n\n        # exactly all sorted_orders to replace (price not going beyond)\n        current_price = decimal.Decimal(\"191\")\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            ((sorted_orders + replaced_orders)[i], decimal.Decimal(str(100 + i * 5)))\n            for i in range(1, len(sorted_orders) + len(replaced_orders))\n        ]\n        # only the other side order is set\n        assert order_to_replace_by_other_side_order == sorted_orders[0]\n        assert other_side_order_price == decimal.Decimal(str(195))\n\n        # all sorted_orders to replace and price is going way beyond\n        current_price = decimal.Decimal(\"253.1\")\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price[-1][1] == decimal.Decimal(str(245))\n        assert orders_to_replace_with_trailed_price == [\n            ((sorted_orders + replaced_orders)[i], decimal.Decimal(str(160 + i * 5))) # 160 to 245\n            for i in range(1, len(sorted_orders) + len(replaced_orders))\n        ]\n        # only the other side order is set\n        assert order_to_replace_by_other_side_order == sorted_orders[0]\n        assert other_side_order_price == decimal.Decimal(str(255))\n\n\nasync def test_get_orders_to_replace_with_updated_price_for_trailing_down():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.flat_increment = decimal.Decimal(\"5\")\n        producer.flat_spread = decimal.Decimal(\"10\")\n        current_price = decimal.Decimal(\"184.1\") # will create sell orders at 95, 90 and a buy order at 80\n        side = trading_enums.TradeOrderSide.SELL\n        quantity = decimal.Decimal(\"0.01\")\n        # 0. no input orders: should not happen, raises\n        with pytest.raises(ValueError):\n            producer._get_orders_to_replace_with_updated_price_for_trailing(\n                [], [], current_price\n            )\n\n        sorted_orders = [\n            trading_personal_data.create_order_instance(\n                exchange_manager.trader, trading_enums.TraderOrderType.SELL_LIMIT, symbol, current_price, quantity, price=decimal.Decimal(str(price))\n            )\n            for price in range (200, 300, 5) # 200 to 295\n        ]\n        # 1. no replaced orders, only existing orders, 4 orders to replace\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, [], current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            (sorted_orders[-2], decimal.Decimal(str(195))),\n            (sorted_orders[-3], decimal.Decimal(str(190))),\n        ]\n        assert order_to_replace_by_other_side_order == sorted_orders[-1]\n        assert other_side_order_price == decimal.Decimal(str(180))\n\n        # 1. replaced orders and existing orders, same result: 4 orders to replace\n        sorted_orders = [\n            trading_personal_data.create_order_instance(\n                exchange_manager.trader, trading_enums.TraderOrderType.BUY_LIMIT, symbol, current_price, quantity, price=decimal.Decimal(str(price))\n            )\n            for price in range (210, 300, 5) # 210 to 295\n        ]\n        replaced_orders = [\n            staggered_orders_trading.OrderData(side, quantity, decimal.Decimal(str(price)), symbol, False)\n            for price in range(200, 210, 5) # 200 to 205\n        ]\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            (sorted_orders[-2], decimal.Decimal(str(195))),\n            (sorted_orders[-3], decimal.Decimal(str(190))),\n        ]\n        assert order_to_replace_by_other_side_order == sorted_orders[-1]\n        assert other_side_order_price == decimal.Decimal(str(180))\n\n        # 2. replaced orders and existing orders, only 1 order to replace\n        for current_price in [decimal.Decimal(\"199.9\"), decimal.Decimal(\"194.9\"), decimal.Decimal(\"195\"), decimal.Decimal(\"190\")]:\n            orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n                sorted_orders, replaced_orders, current_price\n            )\n            assert orders_to_replace_with_trailed_price == []\n            # only the other side order is set\n            assert order_to_replace_by_other_side_order == sorted_orders[-1]\n            assert other_side_order_price == decimal.Decimal(str(190))\n\n        # 3. replaced orders and existing orders, only 2 orders to replace\n        for current_price in [decimal.Decimal(\"188.9\"), decimal.Decimal(\"185.1\"), decimal.Decimal(\"185\")]:\n            orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n                sorted_orders, replaced_orders, current_price\n            )\n            assert orders_to_replace_with_trailed_price == [\n                (sorted_orders[-2], decimal.Decimal(str(195))),\n            ]\n            # only the other side order is set\n            assert order_to_replace_by_other_side_order == sorted_orders[-1]\n            assert other_side_order_price == decimal.Decimal(str(185))\n\n        # all sorted_orders to replace, but not replaced_orders\n        current_price = decimal.Decimal(\"107\")\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            (sorted_orders[-i], decimal.Decimal(str(195 - (i - 2) * 5)))\n            for i in range(2, len(sorted_orders) + 1)\n        ]\n        # only the other side order is set\n        assert order_to_replace_by_other_side_order == sorted_orders[-1]\n        assert other_side_order_price == decimal.Decimal(str(105))\n\n        # exactly all sorted_orders to replace (price not going beyond)\n        current_price = decimal.Decimal(\"96\")\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price == [\n            ((replaced_orders + sorted_orders[:-1])[-i], decimal.Decimal(str(195 - (i - 1) * 5)))\n            for i in range(1, len(sorted_orders) + len(replaced_orders))\n        ]\n        # only the other side order is set\n        assert order_to_replace_by_other_side_order == sorted_orders[-1]\n        assert other_side_order_price == decimal.Decimal(str(95))\n\n        # all sorted_orders to replace and price is going way beyond\n        current_price = decimal.Decimal(\"42.122222222222\")\n        orders_to_replace_with_trailed_price, (order_to_replace_by_other_side_order, other_side_order_price) = producer._get_orders_to_replace_with_updated_price_for_trailing(\n            sorted_orders, replaced_orders, current_price\n        )\n        assert orders_to_replace_with_trailed_price[-1][1] == decimal.Decimal(str(50))\n        assert orders_to_replace_with_trailed_price == [\n            ((replaced_orders + sorted_orders[:-1])[-i], decimal.Decimal(str(140 - (i - 1) * 5))) # 140 to 50\n            for i in range(1, len(sorted_orders) + len(replaced_orders))\n        ]\n        # only the other side order is set\n        assert order_to_replace_by_other_side_order == sorted_orders[-1]\n        assert other_side_order_price == decimal.Decimal(str(40))\n\n\nasync def test_prepare_full_grid_trailing():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        with mock.patch.object(producer, \"_ensure_current_price_in_limit_parameters\", mock.Mock()) \\\n                as _ensure_current_price_in_limit_parameters_mock:\n            await producer._ensure_staggered_orders()\n            _ensure_current_price_in_limit_parameters_mock.assert_called_once()\n        # price info: create orders\n        assert producer.current_price == 4000\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        # now has buy and sell orders\n        open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        # simulate price being stable\n        dependencies = trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\")])\n        log_header = f\"[{producer.exchange_manager.exchange_name}] {producer.symbol} @ {123} full grid trailing process: \"\n        with mock.patch.object(\n            producer.trading_mode, \"cancel_order\", mock.AsyncMock(wraps=producer.trading_mode.cancel_order)\n        ) as cancel_order_mock, mock.patch.object(\n            octobot_trading.modes, \"convert_asset_to_target_asset\", mock.AsyncMock(wraps=octobot_trading.modes.convert_asset_to_target_asset)\n        ) as convert_asset_to_target_asset_mock:\n            cancelled_orders, created_orders, trailing_buy_orders, trailing_sell_orders, end_dependencies = await producer._prepare_full_grid_trailing(\n                open_orders, 4000, dependencies, log_header\n            )\n            assert trailing_buy_orders == trailing_sell_orders == []\n            assert end_dependencies == trading_signals.get_order_dependency(created_orders[0])\n            assert cancel_order_mock.call_count == len(open_orders)\n            assert all(\n                call.kwargs[\"dependencies\"] is dependencies\n                for call in cancel_order_mock.mock_calls\n            )\n            cancelled_orders_dependencies = trading_signals.get_orders_dependencies(\n                [call.args[0] for call in cancel_order_mock.mock_calls]\n            )\n            assert convert_asset_to_target_asset_mock.call_count == 1\n            assert convert_asset_to_target_asset_mock.mock_calls[0].kwargs[\"dependencies\"] == cancelled_orders_dependencies\n        assert len(cancelled_orders) == len(open_orders)\n        # cancelled orders\n        updated_open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        assert updated_open_orders == []\n\n        # created order to balance BTC and USD (sell BTC)\n        assert len(created_orders) == 1\n        assert created_orders[0].symbol == symbol\n        assert created_orders[0].origin_quantity == decimal.Decimal(\"4.87500000\")\n        fees = created_orders[0].fee[trading_enums.FeePropertyColumns.COST.value]\n        assert fees == decimal.Decimal(\"19.5\")\n        assert isinstance(created_orders[0], trading_personal_data.SellMarketOrder)\n\n        portfolio = exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio\n        # portfolio is now balanced\n        assert portfolio[\"BTC\"].available == decimal.Decimal(\"5.125\")   # 5.125 x 4000 = 20500\n        assert portfolio[\"USD\"].available == decimal.Decimal(\"20480.5\") == decimal.Decimal(\"20500\") - fees\n\n        # price change (going down), no order to cancel: just adapt pf\n        trading_api.force_set_mark_price(exchange_manager, symbol, 3000)\n        cancelled_orders, created_orders, trailing_buy_orders, trailing_sell_orders, dependencies = await producer._prepare_full_grid_trailing(open_orders, 3000, dependencies, log_header)\n        assert trailing_buy_orders == trailing_sell_orders == []\n        assert dependencies == trading_signals.get_order_dependency(created_orders[0])\n        # no order to cancel (orders are already cancelled)\n        assert len(cancelled_orders) == 0\n        # created order to balance BTC and USD (buy BTC)\n        assert len(created_orders) == 1\n        assert created_orders[0].symbol == symbol\n        assert created_orders[0].origin_quantity == decimal.Decimal('0.85091666')\n        fees = created_orders[0].fee[trading_enums.FeePropertyColumns.COST.value]\n        assert fees == decimal.Decimal('0.00085091666')\n        assert isinstance(created_orders[0], trading_personal_data.BuyMarketOrder)\n\n        assert portfolio[\"BTC\"].available == decimal.Decimal('5.97506574334')   # Decimal('5.97506574334') x 3000 = Decimal('17925.19723002000')\n        assert portfolio[\"USD\"].available == decimal.Decimal('17927.75002000000')\n\n        # price change (going up), no order to cancel: just adapt pf\n        trading_api.force_set_mark_price(exchange_manager, symbol, 8000)\n        cancelled_orders, created_orders, trailing_buy_orders, trailing_sell_orders, dependencies = await producer._prepare_full_grid_trailing([], 8000, dependencies, log_header)\n        assert trailing_buy_orders == trailing_sell_orders == []\n        assert dependencies == trading_signals.get_order_dependency(created_orders[0])\n        # no order to cancel\n        assert len(cancelled_orders) == 0\n        # created order to balance BTC and USD (buy BTC)\n        assert len(created_orders) == 1\n        assert created_orders[0].symbol == symbol\n        assert created_orders[0].origin_quantity == decimal.Decimal('1.86704849')\n        fees = created_orders[0].fee[trading_enums.FeePropertyColumns.COST.value]\n        assert fees == decimal.Decimal('14.93638792000')\n        assert isinstance(created_orders[0], trading_personal_data.SellMarketOrder)\n\n        assert portfolio[\"BTC\"].available == decimal.Decimal('4.10801725334')   # Decimal('4.10801725334') x 8000 = Decimal('32864.13802672000')\n        assert portfolio[\"USD\"].available == decimal.Decimal('32849.20155208000')\n\n\nasync def test_should_trigger_trailing_not_all_buy_order_created():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        current_price = decimal.Decimal(4000)\n        producer, _, exchange_manager = tools\n        # A. no open order: no trailing\n        assert producer._should_trigger_trailing([], current_price, False) is False\n        producer.enable_trailing_up = producer.enable_trailing_down = True\n        assert producer._should_trigger_trailing([], current_price, False) is False\n\n        # create orders\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        with mock.patch.object(producer, \"_ensure_current_price_in_limit_parameters\", mock.Mock()) \\\n                as _ensure_current_price_in_limit_parameters_mock:\n            await producer._ensure_staggered_orders()\n            _ensure_current_price_in_limit_parameters_mock.assert_called_once()\n\n        assert producer.current_price == 4000\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        # now has buy and sell orders\n\n        # B. trailing disabled\n        producer.enable_trailing_up = producer.enable_trailing_down = False\n        assert producer._should_trigger_trailing([], current_price, False) is False\n        open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]\n        assert len(buy_orders) > 10\n        assert len(sell_orders) > 10\n        assert producer._should_trigger_trailing(buy_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(sell_orders, current_price, False) is False\n\n        # C. trailing enabled\n        producer.enable_trailing_up = True\n        assert producer._should_trigger_trailing(sell_orders, current_price, False) is False\n        # True because all buy orders couldn't be created: impossible to check accurately\n        assert producer._should_trigger_trailing(buy_orders, current_price, False) is True\n\n        producer.enable_trailing_down = True\n        assert producer._should_trigger_trailing(sell_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(sell_orders, decimal.Decimal(100), False) is True\n        assert producer._should_trigger_trailing(buy_orders, current_price, False) is True\n\n        # D. no trailing if at least 1 order on each side\n        assert producer._should_trigger_trailing(buy_orders + sell_orders, current_price, False) is False\n        assert producer._should_trigger_trailing([buy_orders[0]] + sell_orders, current_price, False) is False\n\n        # E. use open orders\n        assert producer._should_trigger_trailing([], current_price, False) is False\n        assert producer._should_trigger_trailing([], current_price, True) is False\n\n\nasync def test_should_trigger_trailing_all_buy_order_created():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        current_price = decimal.Decimal(4000)\n        producer, _, exchange_manager = tools\n        producer.increment = decimal.Decimal(\"0.02\")    # instead of 0.04\n        # A. no open order: no trailing\n        assert producer._should_trigger_trailing([], current_price, False) is False\n        producer.enable_trailing_up = producer.enable_trailing_down = True\n        assert producer._should_trigger_trailing([], current_price, False) is False\n\n        # create orders\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        with mock.patch.object(producer, \"_ensure_current_price_in_limit_parameters\", mock.Mock()) \\\n                as _ensure_current_price_in_limit_parameters_mock:\n            await producer._ensure_staggered_orders()\n            _ensure_current_price_in_limit_parameters_mock.assert_called_once()\n\n        assert producer.current_price == 4000\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n        # now has buy and sell orders\n\n        # B. trailing disabled\n        producer.enable_trailing_up = producer.enable_trailing_down = False\n        assert producer._should_trigger_trailing([], current_price, False) is False\n        open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]\n        assert len(buy_orders) > 10\n        assert len(sell_orders) > 10\n        assert producer._should_trigger_trailing(buy_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(sell_orders, current_price, False) is False\n\n        # C. trailing enabled\n        producer.enable_trailing_up = True\n        assert producer._should_trigger_trailing(sell_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(sell_orders, None, False) is False\n        assert producer._should_trigger_trailing(buy_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(buy_orders, decimal.Decimal(6000), False) is True\n        assert producer._should_trigger_trailing(buy_orders, None, True) is True\n        assert producer._should_trigger_trailing(buy_orders, None, False) is False\n\n        producer.enable_trailing_down = True\n        assert producer._should_trigger_trailing(sell_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(sell_orders, decimal.Decimal(2000), False) is True\n        assert producer._should_trigger_trailing(sell_orders, None, True) is True\n        assert producer._should_trigger_trailing(buy_orders, current_price, False) is False\n        assert producer._should_trigger_trailing(buy_orders, None, False) is False\n        assert producer._should_trigger_trailing(buy_orders, None, True) is True\n\n        # D. no trailing if at least 1 order on each side\n        assert producer._should_trigger_trailing(buy_orders + sell_orders, current_price, False) is False\n        assert producer._should_trigger_trailing([buy_orders[0]] + sell_orders, current_price, False) is False\n\n        # E. use open orders\n        assert producer._should_trigger_trailing([], current_price, False) is False\n        assert producer._should_trigger_trailing([], current_price, True) is False  # has open orders on the other side\n\n\nasync def test_order_notification_callback():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.increment = decimal.Decimal(\"0.02\") # replaces 0.04\n\n        # create orders\n        trading_api.force_set_mark_price(exchange_manager, symbol, 4000)\n        with mock.patch.object(producer, \"_ensure_current_price_in_limit_parameters\", mock.Mock()) \\\n                as _ensure_current_price_in_limit_parameters_mock:\n            await producer._ensure_staggered_orders()\n            _ensure_current_price_in_limit_parameters_mock.assert_called_once()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n\n        # cancel sell orders and change reference price to 6000: should trigger trailing\n        open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]\n        for order in sell_orders:\n            await exchange_manager.trader.cancel_order(order)\n        trading_api.force_set_mark_price(exchange_manager, symbol, 6000)\n        filled_order = buy_orders[0]\n        filled_order.filled_price = 6000\n        producer.enable_trailing_up = producer.enable_trailing_down = False\n        with mock.patch.object(producer, \"_lock_portfolio_and_create_order_when_possible\", mock.AsyncMock()) as _lock_portfolio_and_create_order_when_possible:\n            await _fill_order(filled_order, exchange_manager, trigger_update_callback=True)\n            # trailing disabled\n            _lock_portfolio_and_create_order_when_possible.assert_called_once()\n        assert len(exchange_manager.exchange_personal_data.orders_manager.get_open_orders()) == len(sell_orders) - 1\n\n        # will trail\n        filled_order = buy_orders[1]\n        filled_order.filled_price = 6000\n        producer.use_order_by_order_trailing = False\n        producer.enable_trailing_up = producer.enable_trailing_down = True\n        with mock.patch.object(producer, \"_lock_portfolio_and_create_order_when_possible\", mock.AsyncMock()) as _lock_portfolio_and_create_order_when_possible:\n            await _fill_order(filled_order, exchange_manager, trigger_update_callback=True)\n            # trailing trigger\n            await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n            updated_open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n            buy_orders = [order for order in updated_open_orders if order.side == trading_enums.TradeOrderSide.BUY]\n            sell_orders = [order for order in updated_open_orders if order.side == trading_enums.TradeOrderSide.SELL]\n            assert len(buy_orders) == 25\n            assert len(sell_orders) == 25\n            # trailed instead\n            _lock_portfolio_and_create_order_when_possible.assert_not_called()\n\n        filled_order = buy_orders[0]\n        with mock.patch.object(producer, \"_lock_portfolio_and_create_order_when_possible\", mock.AsyncMock()) as _lock_portfolio_and_create_order_when_possible:\n            await _fill_order(filled_order, exchange_manager, trigger_update_callback=True)\n            # do not trail again, create mirror order instead\n            _lock_portfolio_and_create_order_when_possible.assert_called_once()\n\n\nasync def test_create_mirror_order_considering_exchange_fees():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.ignore_exchange_fees = False\n        # create orders\n        price = 100\n        producer.mode = staggered_orders_trading.StrategyModes.NEUTRAL\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n\n        open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]\n        buy_sell_increment = producer.flat_spread - producer.flat_increment\n\n        # mirroring buy order\n        buy_1 = buy_orders[0]\n        assert buy_1.origin_price == decimal.Decimal(\"97\")\n        assert buy_1.origin_quantity == decimal.Decimal(\"0.46\")\n        assert buy_1.side == trading_enums.TradeOrderSide.BUY\n        buy_1.filled_quantity = buy_1.origin_quantity   # create_mirror order uses filled quantity\n        buy_1_mirror_order = producer._create_mirror_order(buy_1.to_dict())\n        assert isinstance(buy_1_mirror_order, staggered_orders_trading.OrderData)\n        assert buy_1_mirror_order.associated_entry_id == buy_1.order_id\n        assert buy_1_mirror_order.side == trading_enums.TradeOrderSide.SELL\n        assert buy_1_mirror_order.symbol == symbol\n        assert buy_1_mirror_order.price == decimal.Decimal(\"99\") == buy_1.origin_price + buy_sell_increment\n        assert buy_1_mirror_order.quantity < buy_1.origin_quantity  # adapted for exchange fees\n        assert buy_1_mirror_order.quantity == decimal.Decimal('0.4595400')\n\n        # mirroring sell order\n        sell_1 = sell_orders[0]\n        assert sell_1.origin_price == decimal.Decimal(\"103\")\n        assert sell_1.origin_quantity == decimal.Decimal('0.00464646')\n        assert sell_1.side == trading_enums.TradeOrderSide.SELL\n        sell_1.filled_quantity = sell_1.origin_quantity # create_mirror order uses filled quantity\n        sell_1_mirror_order = producer._create_mirror_order(sell_1.to_dict())\n        assert isinstance(sell_1_mirror_order, staggered_orders_trading.OrderData)\n        assert sell_1_mirror_order.associated_entry_id is None\n        assert sell_1_mirror_order.side == trading_enums.TradeOrderSide.BUY\n        assert sell_1_mirror_order.symbol == symbol\n        assert sell_1_mirror_order.price == decimal.Decimal(\"101\") == sell_1.origin_price - buy_sell_increment\n        assert sell_1_mirror_order.quantity > sell_1.origin_quantity\n        assert sell_1_mirror_order.quantity == decimal.Decimal('0.004733730639801980198019801981')\n\n        # fill price is != from origin price => use origin price to avoid moving grid orders\n        assert buy_1.origin_price == decimal.Decimal(\"97\")\n        buy_1.filled_price = decimal.Decimal(\"96\")  # simulate fill at 96\n        buy_2_mirror_order = producer._create_mirror_order(buy_1.to_dict())\n        assert isinstance(buy_2_mirror_order, staggered_orders_trading.OrderData)\n        # mirror order price is still 99, even if fill price is not 97\n        assert buy_2_mirror_order.price == decimal.Decimal(\"99\") == buy_1.origin_price + buy_sell_increment\n        assert buy_2_mirror_order.associated_entry_id == buy_1.order_id\n        assert buy_2_mirror_order.side == trading_enums.TradeOrderSide.SELL\n        # new sell order quantity is equal to previous mirror order quantity: only the amount of USDT spend is smaller\n        assert buy_2_mirror_order.quantity == buy_1_mirror_order.quantity\n        assert buy_2_mirror_order.quantity == decimal.Decimal('0.4595400')\n\n        # sell_1 will be found in trades\n        assert sell_1.origin_price == decimal.Decimal(\"103\")\n        sell_1.filled_price = decimal.Decimal(\"110\")  # simulate fill at 110\n        await _fill_order(sell_1, exchange_manager, trigger_update_callback=False, producer=producer)\n        maybe_trade, maybe_order = exchange_manager.exchange_personal_data.get_trade_or_open_order(\n            sell_1.order_id\n        )\n        assert maybe_trade\n        assert maybe_trade.origin_price == decimal.Decimal(\"103\")\n        assert maybe_order is None\n        sell_2_mirror_order = producer._create_mirror_order(sell_1.to_dict())\n        assert sell_2_mirror_order.associated_entry_id is None\n        # mirror order price is still 101, even if fill price is not 110\n        assert sell_2_mirror_order.price == decimal.Decimal(\"101\") == sell_1.origin_price - buy_sell_increment\n        assert sell_2_mirror_order.side == trading_enums.TradeOrderSide.BUY\n        # new buy order quantity is larger than previous one as sell order was filled at a higher price\n        assert sell_2_mirror_order.quantity > sell_1_mirror_order.quantity\n        assert sell_2_mirror_order.quantity == decimal.Decimal('0.005055854530099009900990099009')\n\n\nasync def test_create_mirror_order_ignoring_exchange_fees():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        producer.ignore_exchange_fees = True\n        # create orders\n        price = 100\n        producer.mode = staggered_orders_trading.StrategyModes.NEUTRAL\n        trading_api.force_set_mark_price(exchange_manager, producer.symbol, price)\n        await producer._ensure_staggered_orders()\n        await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth))\n\n        open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders()\n        buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]\n        sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]\n        buy_sell_increment = producer.flat_spread - producer.flat_increment\n\n        # mirroring buy order with enough remaining funds in portfolio to keep the same quantity\n        buy_1 = buy_orders[0]\n        assert buy_1.origin_price == decimal.Decimal(\"97\")\n        assert buy_1.origin_quantity == decimal.Decimal(\"0.46\")\n        assert buy_1.side == trading_enums.TradeOrderSide.BUY\n        buy_1.filled_quantity = buy_1.origin_quantity   # create_mirror_order uses filled quantity\n        buy_1_mirror_order = producer._create_mirror_order(buy_1.to_dict())\n        assert isinstance(buy_1_mirror_order, staggered_orders_trading.OrderData)\n        assert buy_1_mirror_order.associated_entry_id == buy_1.order_id\n        assert buy_1_mirror_order.side == trading_enums.TradeOrderSide.SELL\n        assert buy_1_mirror_order.symbol == symbol\n        assert buy_1_mirror_order.price == decimal.Decimal(\"99\") == buy_1.origin_price + buy_sell_increment\n        assert buy_1_mirror_order.quantity == buy_1.origin_quantity  # NOT adapted for exchange fees\n        assert buy_1_mirror_order.quantity == decimal.Decimal('0.46')\n\n        # mirroring buy order WITHOUT enough remaining funds in portfolio to keep the same quantity:\n        # => sell order adapted to available funds\n        buy_1 = buy_orders[0]\n        assert buy_1.origin_price == decimal.Decimal(\"97\")\n        assert buy_1.origin_quantity == decimal.Decimal(\"0.46\")\n        assert buy_1.side == trading_enums.TradeOrderSide.BUY\n        buy_1.filled_quantity = buy_1.origin_quantity   # create_mirror_order uses filled quantity\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"BTC\"].available = decimal.Decimal(\"0.3\")\n        buy_1_mirror_order = producer._create_mirror_order(buy_1.to_dict())\n        assert isinstance(buy_1_mirror_order, staggered_orders_trading.OrderData)\n        assert buy_1_mirror_order.associated_entry_id == buy_1.order_id\n        assert buy_1_mirror_order.side == trading_enums.TradeOrderSide.SELL\n        assert buy_1_mirror_order.symbol == symbol\n        assert buy_1_mirror_order.price == decimal.Decimal(\"99\") == buy_1.origin_price + buy_sell_increment\n        assert buy_1_mirror_order.quantity < buy_1.origin_quantity  # adapted for available funds\n        assert buy_1_mirror_order.quantity == decimal.Decimal('0.3')    # equals to available funds\n\n        # => buy order adapted to available funds\n        sell_1 = sell_orders[0]\n        assert sell_1.origin_price == decimal.Decimal(\"103\")\n        assert sell_1.origin_quantity == decimal.Decimal('0.00464646') # cost ~= 0.04\n        assert sell_1.side == trading_enums.TradeOrderSide.SELL\n        sell_1.filled_quantity = sell_1.origin_quantity # create_mirror_order uses filled quantity\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio[\"USD\"].available = decimal.Decimal(\"0.33\")\n        sell_1_mirror_order = producer._create_mirror_order(sell_1.to_dict())\n        assert isinstance(sell_1_mirror_order, staggered_orders_trading.OrderData)\n        assert sell_1_mirror_order.associated_entry_id is None\n        assert sell_1_mirror_order.side == trading_enums.TradeOrderSide.BUY\n        assert sell_1_mirror_order.symbol == symbol\n        assert sell_1_mirror_order.price == decimal.Decimal(\"101\") == sell_1.origin_price - buy_sell_increment\n        assert sell_1_mirror_order.quantity < sell_1.origin_quantity\n        assert sell_1_mirror_order.quantity == decimal.Decimal('0.003267326732673267326732673267')  # adapted to available USDT\n\n\nasync def test_ensure_full_funds_usage():\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol) as tools:\n        producer, _, exchange_manager = tools\n        orders = []\n        # no order, does not raise error\n        assert not producer._ensure_full_funds_usage(orders, 0, 0)\n\n        # funds are from partially filled orders, don't raise\n        buy_order = trading_personal_data.BuyMarketOrder(exchange_manager.trader)\n        buy_order.origin_quantity = decimal.Decimal(10)\n        buy_order.origin_price = decimal.Decimal(100)\n        buy_order.filled_quantity = buy_order.origin_quantity * decimal.Decimal(\"0.99\")\n        sell_order = trading_personal_data.SellMarketOrder(exchange_manager.trader)\n        sell_order.origin_quantity = decimal.Decimal(10)\n        sell_order.origin_price = decimal.Decimal(100)\n        sell_order.filled_quantity = sell_order.origin_quantity * decimal.Decimal(\"0.99\")\n        orders = [buy_order, sell_order]\n        assert not producer._ensure_full_funds_usage(orders, 1, 1)\n\n        # raises\n        buy_order.origin_quantity = decimal.Decimal(6)\n        buy_order.filled_quantity = decimal.Decimal(0)\n        sell_order.origin_quantity = decimal.Decimal(6)\n        sell_order.filled_quantity = decimal.Decimal(0)\n        with pytest.raises(staggered_orders_trading.ForceResetOrdersException):\n            producer._ensure_full_funds_usage([buy_order], 1, 0)\n        with pytest.raises(staggered_orders_trading.ForceResetOrdersException):\n            producer._ensure_full_funds_usage([sell_order], 0, 1)\n        with pytest.raises(staggered_orders_trading.ForceResetOrdersException):\n            producer._ensure_full_funds_usage([buy_order, sell_order], 1, 1)\n\n        # funds are now imbalanced: most of it is in buy orders \n        base, quote = symbol_util.parse_symbol(producer.trading_mode.symbol).base_and_quote()\n        quote_holdings = trading_api.get_portfolio_currency(exchange_manager, quote)\n        base_holdings = trading_api.get_portfolio_currency(exchange_manager, base)\n        quote_holdings.total = decimal.Decimal(1000)\n        quote_holdings.available = decimal.Decimal(100)\n        base_holdings.total = decimal.Decimal(\"0.2\")\n        base_holdings.available = decimal.Decimal(\"0.05\")\n\n        buy_order = trading_personal_data.BuyLimitOrder(exchange_manager.trader)\n        buy_order.origin_quantity = decimal.Decimal(8)\n        buy_order.origin_price = decimal.Decimal(100) # locks 800 USD\n        sell_order = trading_personal_data.SellMarketOrder(exchange_manager.trader)\n        sell_order.origin_quantity = decimal.Decimal(\"0.04\") # locks 0.04 BTC, equivalent to 8 USD, < 0.05 available\n        sell_order.origin_price = decimal.Decimal(200)\n        # doesn't raise as buy orders are locking enough funds and sell order funds are too low to trigger a reset (avoid side effects when reaching the upper edge of the grid)\n        assert not producer._ensure_full_funds_usage([buy_order, sell_order], 1, 1)\n\n        # funds are now imbalanced: most of it is in sell orders \n        quote_holdings.total = decimal.Decimal(80)\n        quote_holdings.available = decimal.Decimal(40)\n        base_holdings.total = decimal.Decimal(\"1\")\n        base_holdings.available = decimal.Decimal(\"0.1\")\n\n        buy_order.origin_quantity = decimal.Decimal(0.1)\n        buy_order.origin_price = decimal.Decimal(100) # locks 10 USD\n        buy_order_2 = trading_personal_data.BuyLimitOrder(exchange_manager.trader)\n        buy_order_2.origin_quantity = decimal.Decimal(0.1)\n        buy_order_2.origin_price = decimal.Decimal(100) # locks 10 USD\n        sell_order.origin_quantity = decimal.Decimal(0.7)\n        sell_order.origin_price = decimal.Decimal(100) # locks 70 USD\n        # doesn't raise as sell orders are locking enough funds and buy order funds are too low to trigger a reset (avoid side effects when reaching the lower edge of the grid)\n        assert not producer._ensure_full_funds_usage([buy_order, buy_order_2, sell_order], 2, 1)\n\n        # now raises when funds are balanced\n        buy_order_3 = trading_personal_data.BuyLimitOrder(exchange_manager.trader)\n        buy_order_3.origin_quantity = decimal.Decimal(0.15)\n        buy_order_3.origin_price = decimal.Decimal(100) # locks 15 USD\n        with pytest.raises(staggered_orders_trading.ForceResetOrdersException):\n            producer._ensure_full_funds_usage([buy_order, buy_order_2, buy_order_3, sell_order], 3, 1)\n\n\nasync def _wait_for_orders_creation(orders_count=1):\n    for _ in range(orders_count):\n        await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def _check_open_orders_count(exchange_manager, count):\n    await _wait_for_orders_creation(count)\n    assert len(trading_api.get_open_orders(exchange_manager)) == count\n\n\ndef _get_total_usd(exchange_manager, btc_price):\n    return trading_api.get_portfolio_currency(exchange_manager, \"USD\", ).total \\\n           + trading_api.get_portfolio_currency(exchange_manager, \"BTC\",\n                                                ).total * btc_price\n\n\nasync def _fill_order(order, exchange_manager, trigger_update_callback=True, producer=None):\n    initial_len = len(trading_api.get_open_orders(exchange_manager))\n    await order.on_fill(force_fill=True)\n    if order.status == trading_enums.OrderStatus.FILLED:\n        assert len(trading_api.get_open_orders(exchange_manager)) == initial_len - 1\n        if trigger_update_callback:\n            # Wait twice so allow `await asyncio_tools.wait_asyncio_next_cycle()` in order.initialize() to finish and complete\n            # order creation AND roll the next cycle that will wake up any pending portfolio lock and allow it to\n            # proceed (here `filled_order_state.terminate()` can be locked if an order has been previously filled AND\n            # a mirror order is being created (and its `await asyncio_tools.wait_asyncio_next_cycle()` in order.initialize()\n            # is pending: in this case `AbstractTradingModeConsumer.create_order_if_possible()` is still\n            # locking the portfolio cause of the previous order's `await asyncio_tools.wait_asyncio_next_cycle()`)).\n            # This lock issue can appear here because we don't use `asyncio_tools.wait_asyncio_next_cycle()` after mirror order\n            # creation (unlike anywhere else in this test file).\n            for _ in range(2):\n                await asyncio_tools.wait_asyncio_next_cycle()\n        else:\n            with mock.patch.object(producer, \"order_filled_callback\", new=mock.AsyncMock()):\n                await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def _test_mode(mode, expected_buy_count, expected_sell_count, price, lowest_buy=None, highest_sell=None,\n                     btc_holdings=None):\n    symbol = \"BTC/USD\"\n    async with _get_tools(symbol, btc_holdings=btc_holdings) as tools:\n        producer, _, exchange_manager = tools\n        if lowest_buy is not None:\n            producer.lowest_buy = decimal.Decimal(str(lowest_buy))\n        if highest_sell is not None:\n            producer.highest_sell = decimal.Decimal(str(highest_sell))\n        producer.mode = mode\n        trading_api.force_set_mark_price(exchange_manager, symbol, price)\n        _, _, _, _, symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager,\n                                                                                   symbol=symbol,\n                                                                                   timeout=1)\n        producer.symbol_market = symbol_market\n        producer.current_price = price\n        orders = await _check_generate_orders(exchange_manager, producer, expected_buy_count,\n                                              expected_sell_count, price, symbol_market)\n\n        await asyncio.create_task(_wait_for_orders_creation(len(orders)))\n        open_orders = trading_api.get_open_orders(exchange_manager)\n        if expected_buy_count or expected_sell_count:\n            assert len(open_orders) <= producer.operational_depth\n        _check_orders(open_orders, mode, producer, exchange_manager)\n\n        assert trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available >= trading_constants.ZERO\n        assert trading_api.get_portfolio_currency(exchange_manager, \"USD\").available >= trading_constants.ZERO\n\n\nasync def _check_generate_orders(exchange_manager, producer, expected_buy_count,\n                                 expected_sell_count, price, symbol_market):\n    async with exchange_manager.exchange_personal_data.portfolio_manager.portfolio.lock:\n        producer._refresh_symbol_data(symbol_market)\n        buy_orders, sell_orders, triggering_trailing, dependencies = await producer._generate_staggered_orders(decimal.Decimal(str(price)), False, False)\n        assert dependencies is None\n        assert len(buy_orders) == expected_buy_count\n        assert len(sell_orders) == expected_sell_count\n        assert triggering_trailing is False\n\n        assert all(o.price < price for o in buy_orders)\n        assert all(o.price > price for o in sell_orders)\n\n        if buy_orders:\n            assert not any(order for order in buy_orders if order.is_virtual)\n\n        if sell_orders:\n            assert any(order for order in sell_orders if order.is_virtual)\n\n        buy_holdings = trading_api.get_portfolio_currency(exchange_manager, \"USD\").available\n        assert sum(order.price * order.quantity for order in buy_orders) <= buy_holdings\n\n        sell_holdings = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").available\n        assert sum(order.quantity for order in sell_orders) <= sell_holdings\n\n        staggered_orders = producer._merged_and_sort_not_virtual_orders(buy_orders, sell_orders)\n        if staggered_orders:\n            assert not any(order for order in staggered_orders if order.is_virtual)\n\n        await producer._create_not_virtual_orders(staggered_orders, price, triggering_trailing, dependencies)\n\n        assert all(producer.highest_sell >= o.price >= producer.lowest_buy\n                   for o in sell_orders)\n\n        assert all(producer.highest_sell >= o.price >= producer.lowest_buy\n                   for o in buy_orders)\n        return staggered_orders\n\n\ndef _check_orders(orders, strategy_mode, producer, exchange_manager):\n    buy_increase_towards_center = staggered_orders_trading.StrategyModeMultipliersDetails[strategy_mode][\n                                      trading_enums.TradeOrderSide.BUY] == staggered_orders_trading.INCREASING\n    sell_increase_towards_center = staggered_orders_trading.StrategyModeMultipliersDetails[strategy_mode][\n                                       trading_enums.TradeOrderSide.SELL] == staggered_orders_trading.INCREASING\n    buy_flat_towards_center = staggered_orders_trading.StrategyModeMultipliersDetails[strategy_mode][\n                                       trading_enums.TradeOrderSide.SELL] == staggered_orders_trading.STABLE\n    sell_flat_towards_center = staggered_orders_trading.StrategyModeMultipliersDetails[strategy_mode][\n                                       trading_enums.TradeOrderSide.SELL] == staggered_orders_trading.STABLE\n    multiplier = staggered_orders_trading.StrategyModeMultipliersDetails[strategy_mode][\n        staggered_orders_trading.MULTIPLIER]\n\n    first_buy = None\n    first_sell = None\n    current_buy = None\n    current_sell = None\n    in_sell_orders = True\n    for order in orders:\n        # first should be sell orders followed by buy orders\n        if order.side is trading_enums.TradeOrderSide.BUY:\n            in_sell_orders = False\n        assert order.side is (trading_enums.TradeOrderSide.SELL if in_sell_orders else trading_enums.TradeOrderSide.BUY)\n\n        if order.side == trading_enums.TradeOrderSide.BUY:\n            if current_buy is None:\n                current_buy = order\n                first_buy = order\n            else:\n                # place buy orders from the lowest price up to the current price\n                assert current_buy.origin_price > order.origin_price\n                if buy_increase_towards_center:\n                    assert current_buy.origin_quantity * current_buy.origin_price > \\\n                           order.origin_quantity * order.origin_price\n                elif buy_flat_towards_center:\n                    assert first_buy.origin_quantity * first_buy.origin_price * decimal.Decimal(\"0.99\")\\\n                           <= current_buy.origin_quantity * current_buy.origin_price \\\n                           <= first_buy.origin_quantity * first_buy.origin_price * decimal.Decimal(\"1.01\")\n                else:\n                    assert current_buy.origin_quantity * current_buy.origin_price < \\\n                           order.origin_quantity * order.origin_price\n                current_buy = order\n\n        if order.side == trading_enums.TradeOrderSide.SELL:\n            if current_sell is None:\n                current_sell = order\n                first_sell = order\n            else:\n                assert current_sell.origin_price < order.origin_price\n                current_sell = order\n                if sell_flat_towards_center:\n                    assert first_sell.origin_quantity * first_sell.origin_price * decimal.Decimal(\"0.99\")\\\n                           <= current_sell.origin_quantity * current_sell.origin_price \\\n                           <= first_sell.origin_quantity * first_sell.origin_price * decimal.Decimal(\"1.01\")\n\n    order_limiting_currency_amount = trading_api.get_portfolio_currency(exchange_manager, \"USD\").total\n    decimal_current_price = decimal.Decimal(str(producer.current_price))\n    _, average_order_quantity = \\\n        producer._get_order_count_and_average_quantity(decimal_current_price,\n                                                       False,\n                                                       producer.lowest_buy,\n                                                       decimal_current_price,\n                                                       decimal.Decimal(str(order_limiting_currency_amount)),\n                                                       \"USD\",\n                                                       strategy_mode)\n    if orders:\n        if buy_increase_towards_center:\n            assert round(multiplier * average_order_quantity * decimal_current_price) - 1 \\\n                   <= round(first_buy.origin_quantity * first_buy.origin_price -\n                            current_buy.origin_quantity * current_buy.origin_price) \\\n                   <= round(multiplier * average_order_quantity * decimal_current_price) + 1\n        else:\n            assert round(multiplier * average_order_quantity * decimal_current_price) - 1 \\\n                   <= round(current_buy.origin_quantity * current_buy.origin_price -\n                            first_buy.origin_quantity * first_buy.origin_price) \\\n                   <= round(multiplier * average_order_quantity * decimal_current_price) + 1\n\n        order_limiting_currency_amount = trading_api.get_portfolio_currency(exchange_manager, \"BTC\").total\n        _, average_order_quantity = \\\n            producer._get_order_count_and_average_quantity(decimal_current_price,\n                                                           True,\n                                                           decimal_current_price,\n                                                           producer.highest_sell,\n                                                           decimal.Decimal(str(order_limiting_currency_amount)),\n                                                           \"BTC\",\n                                                           strategy_mode)\n\n        if strategy_mode not in [staggered_orders_trading.StrategyModes.NEUTRAL,\n                                 staggered_orders_trading.StrategyModes.VALLEY,\n                                 staggered_orders_trading.StrategyModes.SELL_SLOPE]:\n            # not exactly multiplier because of virtual orders and rounds\n            if sell_increase_towards_center:\n                expected_quantity = trading_personal_data.decimal_trunc_with_n_decimal_digits(\n                    average_order_quantity * (1 + multiplier / 2),\n                    8)\n                assert abs(first_sell.origin_quantity - expected_quantity) < \\\n                       multiplier * producer.increment / (2 * decimal_current_price)\n            elif not sell_flat_towards_center:\n                expected_quantity = trading_personal_data.decimal_trunc_with_n_decimal_digits(\n                    average_order_quantity * (1 - multiplier / 2),\n                    8)\n                assert abs(first_sell.origin_quantity - expected_quantity) < \\\n                       multiplier * producer.increment / (2 * decimal_current_price)\n\n\nasync def _light_check_orders(producer, exchange_manager, expected_buy_count, expected_sell_count, price):\n    buy_orders, sell_orders, triggering_trailing, dependencies = await producer._generate_staggered_orders(decimal.Decimal(str(price)), False, False)\n    assert dependencies is None\n    assert len(buy_orders) == expected_buy_count\n    assert len(sell_orders) == expected_sell_count\n    assert triggering_trailing is False\n\n    assert all(o.price < price for o in buy_orders)\n    assert all(o.price > price for o in sell_orders)\n\n    buy_holdings = trading_api.get_portfolio_currency(exchange_manager, \"ETH\").available\n    assert sum(order.price * order.quantity for order in buy_orders) <= buy_holdings\n\n    sell_holdings = trading_api.get_portfolio_currency(exchange_manager, \"RDN\").available\n    assert sum(order.quantity for order in sell_orders) <= sell_holdings\n\n    staggered_orders = producer._merged_and_sort_not_virtual_orders(buy_orders, sell_orders)\n    if staggered_orders:\n        assert not any(order for order in staggered_orders if order.is_virtual)\n\n    await producer._create_not_virtual_orders(staggered_orders, price, triggering_trailing, dependencies)\n\n    await asyncio.create_task(_wait_for_orders_creation(len(staggered_orders)))\n    open_orders = trading_api.get_open_orders(exchange_manager)\n    if expected_buy_count or expected_sell_count:\n        assert len(open_orders) <= producer.operational_depth\n\n    trading_mode = producer.mode\n    buy_increase_towards_center = staggered_orders_trading.StrategyModeMultipliersDetails[trading_mode][\n                                      trading_enums.TradeOrderSide.BUY] == staggered_orders_trading.INCREASING\n\n    current_buy = None\n    current_sell = None\n    in_sell_orders = True\n    for order in open_orders:\n        # first should be sell orders followed by buy orders\n        if order.side is trading_enums.TradeOrderSide.BUY:\n            in_sell_orders = False\n        assert order.side is (trading_enums.TradeOrderSide.SELL if in_sell_orders else trading_enums.TradeOrderSide.BUY)\n\n        if order.side == trading_enums.TradeOrderSide.BUY:\n            if current_buy is None:\n                current_buy = order\n            else:\n                # place buy orders from the current price down to the lowest price\n                assert current_buy.origin_price > order.origin_price\n                if buy_increase_towards_center:\n                    assert current_buy.origin_quantity * current_buy.origin_price > \\\n                           order.origin_quantity * order.origin_price\n                else:\n                    assert current_buy.origin_quantity * current_buy.origin_price < \\\n                           order.origin_quantity * order.origin_price\n                current_buy = order\n\n        if order.side == trading_enums.TradeOrderSide.SELL:\n            if current_sell is None:\n                current_sell = order\n            else:\n                assert current_sell.origin_price < order.origin_price\n                current_sell = order\n\n    assert trading_api.get_portfolio_currency(exchange_manager, \"ETH\").available >= 0\n    assert trading_api.get_portfolio_currency(exchange_manager, \"RDN\").available >= 0\n\n\ndef _get_multi_symbol_staggered_config():\n    return {\n        \"required_strategies\": [],\n        \"pair_settings\": [\n            {\n                \"pair\": \"BTC/USD\",\n                \"mode\": \"mountain\",\n                \"spread_percent\": 4,\n                \"increment_percent\": 3,\n                \"lower_bound\": 4300,\n                \"upper_bound\": 5500,\n                \"allow_instant_fill\": True,\n                \"operational_depth\": 100\n            },\n            {\n                \"pair\": \"ETH/USDT\",\n                \"mode\": \"mountain\",\n                \"spread_percent\": 4,\n                \"increment_percent\": 3,\n                \"lower_bound\": 4300,\n                \"upper_bound\": 5500,\n                \"allow_instant_fill\": True,\n                \"operational_depth\": 100\n            },\n            {\n                \"pair\": \"NANO/USDT\",\n                \"mode\": \"mountain\",\n                \"spread_percent\": 4,\n                \"increment_percent\": 3,\n                \"lower_bound\": 4300,\n                \"upper_bound\": 5500,\n                \"allow_instant_fill\": True,\n                \"operational_depth\": 100\n            }\n        ]\n    }\n"
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/__init__.py",
    "content": "from .trading_view_signals_trading import TradingViewSignalsTradingMode"
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/config/TradingViewSignalsTradingMode.json",
    "content": "{\n    \"close_to_current_price_difference\": 0.02,\n    \"required_strategies\": [],\n    \"use_maximum_size_orders\": false,\n    \"use_market_orders\": true\n}"
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/metadata.json",
    "content": "{\n  \"version\": \"1.2.0\",\n  \"origin_package\": \"OctoBot-Default-Tentacles\",\n  \"tentacles\": [\"TradingViewSignalsTradingMode\"],\n  \"tentacles-requirements\": [\"trading_view_service_feed\"]\n}"
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md",
    "content": "TradingViewSignalsTradingMode is a trading mode configured to automate orders creation on the \nexchange of your choice by following alerts from \n[TradingView](https://www.tradingview.com/?aff_id=27595) price events, indicators or strategies.\n\nFree TradingView <a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-interfaces/tradingview/automating-tradingview-free-email-alerts-with-octobot?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">email</a> \nalerts as well as <a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-interfaces/tradingview/using-a-webhook?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">webhook</a>\nalerts can be used to automate trades based on TradingView alerts.\n\n<div class=\"text-center\">\n    <div>\n    <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/HeOi4PY1ayk\" \n    title=\"TradingView tutorial: automate any strategy with OctoBot custom automation\" frameborder=\"0\" allow=\"accelerometer; autoplay; \n    clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n    </div>\n</div>\n\nTo know more, checkout the \n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/tradingview-trading-mode?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">\nfull TradingView trading mode guide</a>.\n\n### Generate your own strategy using AI\nDescribe your trading strategy to the OctoBot AI strategy generator and get your strategy as Pine Script in seconds.\nAutomate it with your self-hosted OctoBot or a <a\n  href=\"https://app.octobot.cloud/fr/explore?category=tv&utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=tv-trading-mode-tradingview-octobot\"\n  target=\"_blank\" rel=\"noopener\">\n   TradingView OctoBot</a>.\n<p>\n<a class=\"btn btn-primary waves-effect\" \n  href=\"https://app.octobot.cloud/creator?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=tv-trading-mode-generate-my-strategy-with-ai\"\n  target=\"_blank\" rel=\"noopener\">\n   Generate my strategy with AI\n</a>\n</p>\n\n### Alert format cheatsheet\nBasic signals have the following format:\n\n```\nEXCHANGE=BINANCE\nSYMBOL=BTCUSD\nSIGNAL=BUY\n```\n\nAdditional order details can be added to the signal but are optional:\n\n```\nORDER_TYPE=LIMIT\nVOLUME=0.01\nPRICE=42000\nSTOP_PRICE=25000\nTAKE_PROFIT_PRICE=50000\nREDUCE_ONLY=true\n```\n\nWhere:\n- `ORDER_TYPE` is the type of order (LIMIT, MARKET or STOP). Overrides the `Use market orders` parameter\n- `VOLUME` is the volume of the order in base asset (BTC for BTC/USDT) it can a flat amount (ex: `0.1` to trade 0.1 BTC on BTC/USD), \na % of the total portfolio value (ex: `2%`), a % of the available holdings (ex: `12a%`), a % of available holdings associated to the current traded symbol assets (`10s%`) \nor a % of available holdings associated to all configured trading pairs assets (`10t%`). It follows the <a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/order-amount-syntax?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">\norders amount syntax</a>.\n- `PRICE` is the price of the limit order in quote asset (USDT for BTC/USDT). Can also be a delta value from the current price by adding `d` (ex: `10d` or `-0.55d`) or a delta percent from the price (ex: `-5%` or `25.4%`). It follows the <a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/order-price-syntax?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">\norders price syntax</a>.\n- `STOP_PRICE` is the price of the stop order to create. Can also be a delta or % delta like `PRICE`. When increasing the position or buying in spot trading, the stop loss will automatically be created once the initial order is filled. When decreasing the position (or selling in spot) using a LIMIT `ORDER_TYPE`, the stop loss will be created instantly. *Orders crated this way are compatible with PNL history.* It follows the <a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/order-price-syntax?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">\norders price syntax</a>.\n- `TAKE_PROFIT_PRICE` is the price of the take profit order to create. Can also be a delta or % delta like `PRICE`. When increasing the position or buying in spot trading, the take profit will automatically be created once the initial order is filled. When decreasing the position (or selling in spot) using a LIMIT `ORDER_TYPE`, the take profit will be created instantly. *Orders crated this way are compatible with PNL history.* It follows the <a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-trading-modes/order-price-syntax?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">\norders price syntax</a>. Funds will be evenly split between take profits unless a `TAKE_PROFIT_VOLUME_RATIO` is set for each take profit.  \nMultiple take profit prices can be used from `TAKE_PROFIT_PRICE_1`, `TAKE_PROFIT_PRICE_2`, ...\n- `TAKE_PROFIT_VOLUME_RATIO` is the ratio of the entry order volume to include in this take profit. Used when multiple \ntake profits are set. Specify multiple values using `TAKE_PROFIT_VOLUME_RATIO_1`, `TAKE_PROFIT_VOLUME_RATIO_2`, .... When used, a `TAKE_PROFIT_VOLUME_RATIO` is required for each take profit.  \nExemple: `TAKE_PROFIT_PRICE=1234;TAKE_PROFIT_PRICE_1=1456;TAKE_PROFIT_VOLUME_RATIO_1=1;TAKE_PROFIT_VOLUME_RATIO_2=2` will split 33% of entry amount in TP 1 and 67% in TP 2.\n- `REDUCE_ONLY` when true, only reduce the current position (avoid accidental short position opening when reducing a long position). **Only used in futures trading**. Default is false\n- `TAG` is an identifier to give to the orders to create.\n- `LEVERAGE` the leverage value to use when trading futures.\n\nWhen not specified, orders volume and price are automatically computed based on the current \nasset price and holdings.\n\nOrders can be cancelled using the following format:\n``` bash\nEXCHANGE=binance\nSYMBOL=ETHBTC\nSIGNAL=CANCEL\n```\n\nAdditional cancel parameters:\n- `PARAM_SIDE` is the side of the orders to cancel, it can be `buy` or `sell` to only cancel buy or sell orders.\n- `TAG` is the tag of the order(s) to cancel. It can be used to only cancel orders that have been created with a specific tag.\n\nNote: `;` can also be used to separate signal parameters, exemple: `EXCHANGE=binance;SYMBOL=ETHBTC;SIGNAL=CANCEL` is equivalent to the previous example.\n\nFind the full TradingView alerts format on\n<a target=\"_blank\" rel=\"noopener\" href=\"https://www.octobot.cloud/en/guides/octobot-interfaces/tradingview/alert-format?utm_source=octobot&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=TradingViewSignalsTradingModeDocs\">\nthe TradingView alerts format guide</a>.\n\n"
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/tests/__init__.py",
    "content": ""
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport math\n\nimport mock\nimport pytest\nimport os.path\nimport pytest_asyncio\n\nimport async_channel.util as channel_util\nimport octobot_backtesting.api as backtesting_api\nimport octobot_commons.asyncio_tools as asyncio_tools\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.symbols as commons_symbols\nimport octobot_commons.tests.test_config as test_config\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.api as trading_api\nimport octobot_trading.exchange_channel as exchanges_channel\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as exchanges\nimport octobot_trading.errors as errors\nimport octobot_trading.personal_data as trading_personal_data\nimport octobot_trading.signals as trading_signals\nimport octobot_trading.modes.script_keywords as script_keywords\nimport tentacles.Trading.Mode as Mode\nimport tests.test_utils.config as test_utils_config\nimport tests.test_utils.test_exchanges as test_exchanges\nimport octobot_tentacles_manager.api as tentacles_manager_api\n\n\n# All test coroutines will be treated as marked.\npytestmark = pytest.mark.asyncio\n\n\n@pytest_asyncio.fixture\nasync def tools():\n    tentacles_manager_api.reload_tentacle_info()\n    exchange_manager = None\n    try:\n        symbol = \"BTC/USDT\"\n        config = test_config.load_test_config()\n        config[commons_constants.CONFIG_SIMULATOR][commons_constants.CONFIG_STARTING_PORTFOLIO][\"USDT\"] = 2000\n        exchange_manager = test_exchanges.get_test_exchange_manager(config, \"binance\")\n        exchange_manager.tentacles_setup_config = test_utils_config.get_tentacles_setup_config()\n\n        # use backtesting not to spam exchanges apis\n        exchange_manager.is_simulated = True\n        exchange_manager.is_backtesting = True\n        exchange_manager.use_cached_markets = False\n        backtesting = await backtesting_api.initialize_backtesting(\n            config,\n            exchange_ids=[exchange_manager.id],\n            matrix_id=None,\n            data_files=[\n                os.path.join(test_config.TEST_CONFIG_FOLDER, \"AbstractExchangeHistoryCollector_1586017993.616272.data\")\n            ])\n        exchange_manager.exchange = exchanges.ExchangeSimulator(exchange_manager.config,\n                                                                exchange_manager,\n                                                                backtesting)\n        await exchange_manager.exchange.initialize()\n        for exchange_channel_class_type in [exchanges_channel.ExchangeChannel, exchanges_channel.TimeFrameExchangeChannel]:\n            await channel_util.create_all_subclasses_channel(exchange_channel_class_type, exchanges_channel.set_chan,\n                                                             exchange_manager=exchange_manager)\n\n        trader = exchanges.TraderSimulator(config, exchange_manager)\n        await trader.initialize()\n\n        mode = Mode.TradingViewSignalsTradingMode(config, exchange_manager)\n        mode.symbol = symbol\n        await mode.initialize()\n        # add mode to exchange manager so that it can be stopped and freed from memory\n        exchange_manager.trading_modes.append(mode)\n        producer = mode.producers[0]\n        consumer = mode.get_trading_mode_consumers()[0]\n\n        # set BTC/USDT price at 7009.194999999998 USDT\n        last_btc_price = 7009.194999999998\n        trading_api.force_set_mark_price(exchange_manager, symbol, last_btc_price)\n        exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \\\n            decimal.Decimal(str(last_btc_price * 10))\n\n        yield exchange_manager, symbol, mode, producer, consumer\n    finally:\n        if exchange_manager:\n            try:\n                await _stop(exchange_manager)\n            except Exception as err:\n                print(f\"error when stopping exchange manager: {err}\")\n\n\nasync def _stop(exchange_manager):\n    for importer in backtesting_api.get_importers(exchange_manager.exchange.backtesting):\n        await backtesting_api.stop_importer(importer)\n    await exchange_manager.exchange.backtesting.stop()\n    await exchange_manager.stop()\n    # let updaters gracefully shutdown\n    await asyncio_tools.wait_asyncio_next_cycle()\n\n\nasync def test_parse_signal_data():\n    errors = []\n    assert Mode.TradingViewSignalsTradingMode.parse_signal_data(\n        \"\"\"\n        \n        \n        KEY=value\n        EXCHANGE=1\n        \n        \n        \n        PLOp=true\n        \"\"\",\n        errors\n    ) == {\n        \"KEY\": \"value\",\n        \"EXCHANGE\": \"1\",\n        \"PLOp\": True,\n    }\n    assert errors == []\n\n    errors = []\n    assert Mode.TradingViewSignalsTradingMode.parse_signal_data(\n        \"KEY=value\\nEXCHANGE=1\\n\\n\\n\\nPLOp=false\\n\",\n        errors\n    ) == {\n        \"KEY\": \"value\",\n        \"EXCHANGE\": \"1\",\n        \"PLOp\": False,\n    }\n    assert errors == []\n\n    errors = []\n    assert Mode.TradingViewSignalsTradingMode.parse_signal_data(\n        \"KEY=value\\\\nEXCHANGE=1\\\\nPLOp=ABC\",\n        errors\n    ) == {\n        \"KEY\": \"value\",\n        \"EXCHANGE\": \"1\",\n        \"PLOp\": \"ABC\",\n    }\n    assert errors == []\n\n    errors = []\n    assert Mode.TradingViewSignalsTradingMode.parse_signal_data(\n        \"KEY=value\\\\nEXCHANGE\\\\nPLOp=ABC\",\n        errors\n    ) == {\n        \"KEY\": \"value\",\n        \"PLOp\": \"ABC\",\n    }\n    assert len(errors) == 1\n    assert \"EXCHANGE\" in str(errors[0])\n    assert \"nPLOp\" not in str(errors[0])\n    assert \"KEY\" not in str(errors[0])\n\n    errors = []\n    assert Mode.TradingViewSignalsTradingMode.parse_signal_data(\n        \"KEY=value;EXCHANGE;;;;;PLOp=ABC;TAKE_PROFIT_PRICE=1;;TAKE_PROFIT_PRICE_2=3\",\n        errors\n    ) == {\n        \"KEY\": \"value\",\n        \"PLOp\": \"ABC\",\n        \"TAKE_PROFIT_PRICE\": \"1\",\n        \"TAKE_PROFIT_PRICE_2\": \"3\",\n    }\n    assert len(errors) == 1\n    assert \"EXCHANGE\" in str(errors[0])\n    assert \"nPLOp\" not in str(errors[0])\n    assert \"KEY\" not in str(errors[0])\n\n    errors = []\n    assert Mode.TradingViewSignalsTradingMode.parse_signal_data(\n        \";KEY=value;EXCHANGE\\nPLOp=ABC\\\\nGG=HIHI;LEVERAGE=3\",\n        errors\n    ) == {\n        \"KEY\": \"value\",\n        \"PLOp\": \"ABC\",\n        \"GG\": \"HIHI\",\n        \"LEVERAGE\": \"3\",\n    }\n    assert len(errors) == 1\n    assert \"EXCHANGE\" in str(errors[0])\n    assert \"nPLOp\" not in str(errors[0])\n    assert \"KEY\" not in str(errors[0])\n    assert \"LEVERAGE\" not in str(errors[0])\n\n\nasync def test_trading_view_signal_callback(tools):\n    exchange_manager, symbol, mode, producer, consumer = tools\n    context = script_keywords.get_base_context(producer.trading_mode)\n    with mock.patch.object(script_keywords, \"get_base_context\", mock.Mock(return_value=context)) \\\n         as get_base_context_mock:\n        for exception in (errors.MissingFunds, errors.InvalidArgumentError):\n            # ensure exception is caught\n            with mock.patch.object(\n                    producer, \"signal_callback\", mock.AsyncMock(side_effect=exception)\n            ) as signal_callback_mock:\n                signal = f\"\"\"\n                    EXCHANGE={exchange_manager.exchange_name}\n                    SYMBOL={symbol}\n                    SIGNAL=BUY\n                \"\"\"\n                await mode._trading_view_signal_callback({\"metadata\": signal})\n                signal_callback_mock.assert_awaited_once()\n                get_base_context_mock.assert_called_once()\n                get_base_context_mock.reset_mock()\n\n        with mock.patch.object(producer, \"signal_callback\", mock.AsyncMock()) as signal_callback_mock:\n            # invalid data\n            data = \"\"\n            await mode._trading_view_signal_callback({\"metadata\": data})\n            signal_callback_mock.assert_not_awaited()\n            signal_callback_mock.reset_mock()\n            get_base_context_mock.assert_not_called()\n\n            # invalid symbol\n            data = f\"\"\"\n            EXCHANGE={exchange_manager.exchange_name}\n            SYMBOL={symbol}PLOP\n            SIGNAL=BUY\n            \"\"\"\n            await mode._trading_view_signal_callback({\"metadata\": data})\n            signal_callback_mock.assert_not_awaited()\n            signal_callback_mock.reset_mock()\n            get_base_context_mock.assert_not_called()\n\n            # minimal signal\n            data = f\"\"\"\n            EXCHANGE={exchange_manager.exchange_name}\n            SYMBOL={symbol}\n            SIGNAL=BUY\n            \"\"\"\n            await mode._trading_view_signal_callback({\"metadata\": data})\n            signal_callback_mock.assert_awaited_once_with({\n                mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n                mode.SYMBOL_KEY: symbol,\n                mode.SIGNAL_KEY: \"BUY\",\n            }, context)\n            signal_callback_mock.reset_mock()\n            get_base_context_mock.assert_called_once()\n            get_base_context_mock.reset_mock()\n\n            # minimal signal\n            signal = f\"\"\"\n                EXCHANGE={exchange_manager.exchange_name}\n                SYMBOL={symbol}\n                SIGNAL=BUY\n            \"\"\"\n            await mode._trading_view_signal_callback({\"metadata\": signal})\n            signal_callback_mock.assert_awaited_once_with({\n                mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n                mode.SYMBOL_KEY: symbol,\n                mode.SIGNAL_KEY: \"BUY\",\n            }, context)\n            signal_callback_mock.reset_mock()\n            get_base_context_mock.assert_called_once()\n            get_base_context_mock.reset_mock()\n\n            # other signals\n            signal = f\"\"\"\n                EXCHANGE={exchange_manager.exchange_name}\n                SYMBOL={commons_symbols.parse_symbol(symbol).merged_str_base_and_quote_only_symbol(\n                market_separator=\"\"\n            )}\n                SIGNAL=BUY\n                HEELLO=True\n                PLOP=faLse\n            \"\"\"\n            await mode._trading_view_signal_callback({\"metadata\": signal})\n            signal_callback_mock.assert_awaited_once_with({\n                mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n                mode.SYMBOL_KEY: commons_symbols.parse_symbol(symbol).merged_str_base_and_quote_only_symbol(\n                    market_separator=\"\"\n                ),\n                mode.SIGNAL_KEY: \"BUY\",\n                \"HEELLO\": True,\n                \"PLOP\": False,\n            }, context)\n            signal_callback_mock.reset_mock()\n            get_base_context_mock.assert_called_once()\n            get_base_context_mock.reset_mock()\n\n\nasync def test_signal_callback(tools):\n    exchange_manager, symbol, mode, producer, consumer = tools\n    context = script_keywords.get_base_context(producer.trading_mode)\n    with mock.patch.object(producer, \"_set_state\", mock.AsyncMock()) as _set_state_mock, \\\n        mock.patch.object(mode, \"set_leverage\", mock.AsyncMock()) as set_leverage_mock:\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"BUY\",\n        }, context)\n        _set_state_mock.assert_awaited_once()\n        set_leverage_mock.assert_not_called()\n        assert _set_state_mock.await_args[0][1] == symbol\n        assert _set_state_mock.await_args[0][2] == trading_enums.EvaluatorStates.VERY_LONG\n        assert compare_dict_with_nan(_set_state_mock.await_args[0][3], {\n            consumer.PRICE_KEY: trading_constants.ZERO,\n            consumer.VOLUME_KEY: trading_constants.ZERO,\n            consumer.STOP_PRICE_KEY: decimal.Decimal(math.nan),\n            consumer.STOP_ONLY: False,\n            consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(math.nan),\n            consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [],\n            consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: [],\n            consumer.REDUCE_ONLY_KEY: False,\n            consumer.TAG_KEY: None,\n            consumer.TRAILING_PROFILE: None,\n            consumer.EXCHANGE_ORDER_IDS: None,\n            consumer.LEVERAGE: None,\n            consumer.ORDER_EXCHANGE_CREATION_PARAMS: {},\n            consumer.CANCEL_POLICY: None,\n            consumer.CANCEL_POLICY_PARAMS: None,\n        })\n        _set_state_mock.reset_mock()\n\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"SELL\",\n            mode.ORDER_TYPE_SIGNAL: \"stop\",\n            mode.STOP_PRICE_KEY: 25000,\n            mode.VOLUME_KEY: \"12%\",\n            mode.TAG_KEY: \"stop_1_tag\",\n            mode.CANCEL_POLICY: trading_personal_data.ExpirationTimeOrderCancelPolicy.__name__,\n            mode.CANCEL_POLICY_PARAMS: {\n                \"expiration_time\": 1000.0,\n            },\n            consumer.EXCHANGE_ORDER_IDS: None,\n\n        }, context)\n        set_leverage_mock.assert_not_called()\n        _set_state_mock.assert_awaited_once()\n        assert _set_state_mock.await_args[0][1] == symbol\n        assert _set_state_mock.await_args[0][2] == trading_enums.EvaluatorStates.SHORT\n        assert compare_dict_with_nan(_set_state_mock.await_args[0][3], {\n            consumer.PRICE_KEY: trading_constants.ZERO,\n            consumer.VOLUME_KEY: decimal.Decimal(\"1.2\"),\n            consumer.STOP_PRICE_KEY: decimal.Decimal(\"25000\"),\n            consumer.STOP_ONLY: True,\n            consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(math.nan),\n            consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [],\n            consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: [],\n            consumer.REDUCE_ONLY_KEY: False,\n            consumer.TAG_KEY: \"stop_1_tag\",\n            consumer.EXCHANGE_ORDER_IDS: None,\n            consumer.TRAILING_PROFILE: None,\n            consumer.LEVERAGE: None,\n            consumer.ORDER_EXCHANGE_CREATION_PARAMS: {},\n            consumer.CANCEL_POLICY: trading_personal_data.ExpirationTimeOrderCancelPolicy.__name__,\n            consumer.CANCEL_POLICY_PARAMS: {'expiration_time': 1000.0},\n        })\n        _set_state_mock.reset_mock()\n\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"SelL\",\n            mode.PRICE_KEY: \"123\",\n            mode.VOLUME_KEY: \"12%\",\n            mode.REDUCE_ONLY_KEY: True,\n            mode.ORDER_TYPE_SIGNAL: \"LiMiT\",\n            mode.STOP_PRICE_KEY: \"12\",\n            mode.TAKE_PROFIT_PRICE_KEY: \"22222\",\n            mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n            mode.CANCEL_POLICY: \"chainedorderfillingpriceordercancelpolicy\",\n            consumer.LEVERAGE: 22,\n            \"PARAM_TAG_1\": \"ttt\",\n            \"PARAM_Plop\": False,\n        }, context)\n        set_leverage_mock.assert_called_once()\n        assert set_leverage_mock.mock_calls[0].args[2] == decimal.Decimal(22)\n        set_leverage_mock.reset_mock()\n        _set_state_mock.assert_awaited_once()\n        assert _set_state_mock.await_args[0][1] == symbol\n        assert _set_state_mock.await_args[0][2] == trading_enums.EvaluatorStates.SHORT\n        assert compare_dict_with_nan(_set_state_mock.await_args[0][3], {\n            consumer.PRICE_KEY: decimal.Decimal(\"123\"),\n            consumer.VOLUME_KEY: decimal.Decimal(\"1.2\"),\n            consumer.STOP_PRICE_KEY: decimal.Decimal(\"12\"),\n            consumer.STOP_ONLY: False,\n            consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"22222\"),\n            consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [],\n            consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: [],\n            consumer.REDUCE_ONLY_KEY: True,\n            consumer.TAG_KEY: None,\n            mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n            consumer.TRAILING_PROFILE: None,\n            consumer.LEVERAGE: 22,\n            consumer.ORDER_EXCHANGE_CREATION_PARAMS: {\n                \"TAG_1\": \"ttt\",\n                \"Plop\": False,\n            },\n            consumer.CANCEL_POLICY: trading_personal_data.ChainedOrderFillingPriceOrderCancelPolicy.__name__,\n            consumer.CANCEL_POLICY_PARAMS: None,\n        })\n        _set_state_mock.reset_mock()\n\n        # with trailing profile and TP volume\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"SelL\",\n            mode.PRICE_KEY: \"123\",\n            mode.VOLUME_KEY: \"12%\",\n            mode.REDUCE_ONLY_KEY: True,\n            mode.ORDER_TYPE_SIGNAL: \"LiMiT\",\n            mode.STOP_PRICE_KEY: \"12\",\n            mode.TAKE_PROFIT_PRICE_KEY: \"22222\",\n            mode.TAKE_PROFIT_VOLUME_RATIO_KEY: \"1\",\n            mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n            mode.TRAILING_PROFILE: \"fiLLED_take_profit\",\n            mode.CANCEL_POLICY: \"expirationtimeordercancelpolicy\",\n            mode.CANCEL_POLICY_PARAMS: \"{'expiration_time': 1000.0}\",\n            consumer.LEVERAGE: 22,\n            \"PARAM_TAG_1\": \"ttt\",\n            \"PARAM_Plop\": False,\n        }, context)\n        set_leverage_mock.assert_called_once()\n        assert set_leverage_mock.mock_calls[0].args[2] == decimal.Decimal(22)\n        set_leverage_mock.reset_mock()\n        _set_state_mock.assert_awaited_once()\n        assert _set_state_mock.await_args[0][1] == symbol\n        assert _set_state_mock.await_args[0][2] == trading_enums.EvaluatorStates.SHORT\n        assert compare_dict_with_nan(_set_state_mock.await_args[0][3], {\n            consumer.PRICE_KEY: decimal.Decimal(\"123\"),\n            consumer.VOLUME_KEY: decimal.Decimal(\"1.2\"),\n            consumer.STOP_PRICE_KEY: decimal.Decimal(\"12\"),\n            consumer.STOP_ONLY: False,\n            consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"22222\"),\n            consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [],\n            consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: [decimal.Decimal(1)],\n            consumer.REDUCE_ONLY_KEY: True,\n            consumer.TAG_KEY: None,\n            mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n            consumer.LEVERAGE: 22,\n            consumer.TRAILING_PROFILE: \"filled_take_profit\",\n            consumer.ORDER_EXCHANGE_CREATION_PARAMS: {\n                \"TAG_1\": \"ttt\",\n                \"Plop\": False,\n            },\n            consumer.CANCEL_POLICY: trading_personal_data.ExpirationTimeOrderCancelPolicy.__name__,\n            consumer.CANCEL_POLICY_PARAMS: {'expiration_time': 1000.0},\n        })\n        _set_state_mock.reset_mock()\n\n        # future exchange: call set_leverage\n        exchange_manager.is_future = True\n        trading_api.load_pair_contract(\n            exchange_manager,\n            trading_api.create_default_future_contract(\n                \"BTC/USDT\", decimal.Decimal(4), trading_enums.FutureContractType.LINEAR_PERPETUAL,\n                trading_constants.DEFAULT_SYMBOL_POSITION_MODE\n            ).to_dict()\n        )\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"SelL\",\n            mode.PRICE_KEY: \"123@\",  # price = 123\n            mode.VOLUME_KEY: \"100q\",  # base amount\n            mode.REDUCE_ONLY_KEY: False,\n            mode.ORDER_TYPE_SIGNAL: \"LiMiT\",\n            mode.STOP_PRICE_KEY: \"-10%\",  # price - 10%\n            f\"{mode.TAKE_PROFIT_PRICE_KEY}_0\": \"120.333333333333333d\",   # price  + 120.333333333333333\n            f\"{mode.TAKE_PROFIT_PRICE_KEY}_1\": \"122.333333333333333d\",   # price  + 122.333333333333333\n            f\"{mode.TAKE_PROFIT_PRICE_KEY}_2\": \"4444d\",   # price  + 4444\n            f\"{mode.TAKE_PROFIT_VOLUME_RATIO_KEY}_0\": \"1\",\n            f\"{mode.TAKE_PROFIT_VOLUME_RATIO_KEY}_1\": \"1.122\",\n            f\"{mode.TAKE_PROFIT_VOLUME_RATIO_KEY}_2\": \"0.2222\",\n            mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n            consumer.LEVERAGE: 22,\n            \"PARAM_TAG_1\": \"ttt\",\n            \"PARAM_Plop\": False,\n        }, context)\n        set_leverage_mock.assert_called_once()\n        assert set_leverage_mock.mock_calls[0].args[2] == decimal.Decimal(\"22\")\n        _set_state_mock.assert_awaited_once()\n        assert _set_state_mock.await_args[0][1] == symbol\n        assert _set_state_mock.await_args[0][2] == trading_enums.EvaluatorStates.SHORT\n        assert compare_dict_with_nan(_set_state_mock.await_args[0][3], {\n            consumer.PRICE_KEY: decimal.Decimal(\"123\"),\n            consumer.VOLUME_KEY: decimal.Decimal(\"0.8130081300813008130081300813\"),\n            consumer.STOP_PRICE_KEY: decimal.Decimal(\"6308.27549999\"),\n            consumer.STOP_ONLY: False,\n            consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(\"nan\"), # only additional TP orders are provided\n            consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [\n                decimal.Decimal(\"7129.52833333\"), decimal.Decimal(\"7131.52833333\"), decimal.Decimal('11453.19499999')\n            ],\n            consumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: [\n                decimal.Decimal(\"1\"), decimal.Decimal(\"1.122\"), decimal.Decimal(\"0.2222\"),\n            ],\n            consumer.REDUCE_ONLY_KEY: False,\n            consumer.TAG_KEY: None,\n            mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n            consumer.TRAILING_PROFILE: None,\n            consumer.LEVERAGE: 22,\n            consumer.ORDER_EXCHANGE_CREATION_PARAMS: {\n                \"TAG_1\": \"ttt\",\n                \"Plop\": False,\n            },\n            consumer.CANCEL_POLICY: None,\n            consumer.CANCEL_POLICY_PARAMS: None,\n        })\n        _set_state_mock.reset_mock()\n        set_leverage_mock.reset_mock()\n\n        with pytest.raises(errors.MissingFunds):\n            await producer.signal_callback({\n                mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n                mode.SYMBOL_KEY: \"unused\",\n                mode.SIGNAL_KEY: \"SelL\",\n                mode.PRICE_KEY: \"123000q\",  # price = 123\n                mode.VOLUME_KEY: \"11111b\",  # base amount: not enough funds\n                mode.REDUCE_ONLY_KEY: True,\n                mode.ORDER_TYPE_SIGNAL: \"LiMiT\",\n                mode.STOP_PRICE_KEY: \"-10%\",  # price - 10%\n                mode.TAKE_PROFIT_PRICE_KEY: \"120.333333333333333d\",   # price  + 120.333333333333333\n                mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n                mode.LEVERAGE: None,\n                \"PARAM_TAG_1\": \"ttt\",\n                \"PARAM_Plop\": False,\n            }, context)\n        set_leverage_mock.assert_not_called()\n        _set_state_mock.assert_not_called()\n\n        with pytest.raises(errors.InvalidArgumentError):\n            await producer.signal_callback({\n                mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n                mode.SYMBOL_KEY: \"unused\",\n                mode.SIGNAL_KEY: \"DSDSDDSS\",\n                mode.PRICE_KEY: \"123000q\",  # price = 123\n                mode.VOLUME_KEY: \"11111b\",  # base amount: not enough funds\n                mode.REDUCE_ONLY_KEY: True,\n                mode.ORDER_TYPE_SIGNAL: \"LiMiT\",\n                mode.STOP_PRICE_KEY: \"-10%\",  # price - 10%\n                mode.TAKE_PROFIT_PRICE_KEY: \"120.333333333333333d\",   # price  + 120.333333333333333\n                mode.EXCHANGE_ORDER_IDS: [\"ab1\", \"aaaaa\"],\n                mode.LEVERAGE: None,\n                \"PARAM_TAG_1\": \"ttt\",\n                \"PARAM_Plop\": False,\n            }, context)\n        set_leverage_mock.assert_not_called()\n        _set_state_mock.assert_not_called()\n\n        with pytest.raises(errors.InvalidCancelPolicyError):\n            await producer.signal_callback({\n                mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n                mode.SYMBOL_KEY: \"unused\",\n                mode.SIGNAL_KEY: \"SelL\",\n                mode.CANCEL_POLICY: \"unknown_cancel_policy\",\n            }, context)\n        set_leverage_mock.assert_not_called()\n        _set_state_mock.assert_not_called()\n\n\nasync def test_signal_callback_with_cancel_policies(tools):\n    exchange_manager, symbol, mode, producer, consumer = tools\n    context = script_keywords.get_base_context(producer.trading_mode)\n    mode.CANCEL_PREVIOUS_ORDERS = True\n\n    async def _apply_cancel_policies(*args, **kwargs):\n        return True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"123\"), mock.Mock(order_id=\"456-cancel_policy\")])\n    async def _cancel_symbol_open_orders(*args, **kwargs):\n        return True, trading_signals.get_orders_dependencies([mock.Mock(order_id=\"456-cancel_symbol_open_orders\")])\n\n    with mock.patch.object(producer, \"_set_state\", mock.AsyncMock()) as _set_state_mock, \\\n        mock.patch.object(producer, \"_process_pre_state_update_actions\", mock.AsyncMock()) as _process_pre_state_update_actions_mock, \\\n        mock.patch.object(producer, \"_parse_order_details\", mock.AsyncMock(return_value=(trading_enums.EvaluatorStates.LONG, {}))) as _parse_order_details_mock, \\\n        mock.patch.object(producer, \"apply_cancel_policies\", mock.AsyncMock(side_effect=_apply_cancel_policies)) as apply_cancel_policies_mock, \\\n        mock.patch.object(producer, \"cancel_symbol_open_orders\", mock.AsyncMock(side_effect=_cancel_symbol_open_orders)) as cancel_symbol_open_orders_mock:\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"BUY\",\n        }, context)\n        _process_pre_state_update_actions_mock.assert_awaited_once()\n        _parse_order_details_mock.assert_awaited_once()\n        apply_cancel_policies_mock.assert_awaited_once()\n        cancel_symbol_open_orders_mock.assert_awaited_once()\n        _set_state_mock.assert_awaited_once()\n        assert _set_state_mock.mock_calls[0].kwargs[\"dependencies\"] == trading_signals.get_orders_dependencies([\n            mock.Mock(order_id=\"123\"), \n            mock.Mock(order_id=\"456-cancel_policy\"), \n            mock.Mock(order_id=\"456-cancel_symbol_open_orders\")\n        ])\n    mode.CANCEL_PREVIOUS_ORDERS = False\n    with mock.patch.object(producer, \"_set_state\", mock.AsyncMock()) as _set_state_mock, \\\n        mock.patch.object(producer, \"_process_pre_state_update_actions\", mock.AsyncMock()) as _process_pre_state_update_actions_mock, \\\n        mock.patch.object(producer, \"_parse_order_details\", mock.AsyncMock(return_value=(trading_enums.EvaluatorStates.LONG, {}))) as _parse_order_details_mock, \\\n        mock.patch.object(producer, \"apply_cancel_policies\", mock.AsyncMock(side_effect=_apply_cancel_policies)) as apply_cancel_policies_mock, \\\n        mock.patch.object(producer, \"cancel_symbol_open_orders\", mock.AsyncMock(side_effect=_cancel_symbol_open_orders)) as cancel_symbol_open_orders_mock:\n        await producer.signal_callback({\n            mode.EXCHANGE_KEY: exchange_manager.exchange_name,\n            mode.SYMBOL_KEY: \"unused\",\n            mode.SIGNAL_KEY: \"BUY\",\n        }, context)\n        _process_pre_state_update_actions_mock.assert_awaited_once()\n        _parse_order_details_mock.assert_awaited_once()\n        apply_cancel_policies_mock.assert_awaited_once()\n        cancel_symbol_open_orders_mock.assert_not_called() # CANCEL_PREVIOUS_ORDERS is False\n        _set_state_mock.assert_awaited_once()\n        assert _set_state_mock.mock_calls[0].kwargs[\"dependencies\"] == trading_signals.get_orders_dependencies([\n            mock.Mock(order_id=\"123\"), \n            mock.Mock(order_id=\"456-cancel_policy\"),\n        ])\n\n\ndef compare_dict_with_nan(d_1, d_2):\n    try:\n        for key, val in d_1.items():\n            assert (\n                d_2[key] == d_1[key]\n                or (isinstance(d_2[key], decimal.Decimal) and d_2[key].is_nan() and isinstance(d_1[key], decimal.Decimal) and d_1[key].is_nan())\n                or compare_dict_with_nan(d_1[key], d_2[key])\n            ), f\"Key {key} is not equal: {d_1[key]} != {d_2[key]}\"\n        return True\n    except (KeyError, AttributeError) as err:\n        # print(f\"Error comparing dicts: {err.__class__.__name__}: {err}\")\n        return False\n"
  },
  {
    "path": "Trading/Mode/trading_view_signals_trading_mode/trading_view_signals_trading.py",
    "content": "#  Drakkar-Software OctoBot-Tentacles\n#  Copyright (c) Drakkar-Software, All rights reserved.\n#\n#  This library is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU Lesser General Public\n#  License as published by the Free Software Foundation; either\n#  version 3.0 of the License, or (at your option) any later version.\n#\n#  This library is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n#  Lesser General Public License for more details.\n#\n#  You should have received a copy of the GNU Lesser General Public\n#  License along with this library.\nimport decimal\nimport math\nimport typing\nimport json\nimport copy\n\nimport async_channel.channels as channels\nimport octobot_commons.symbols.symbol_util as symbol_util\nimport octobot_commons.enums as commons_enums\nimport octobot_commons.constants as commons_constants\nimport octobot_commons.signals as commons_signals\nimport octobot_commons.tentacles_management as tentacles_management\nimport octobot_services.api as services_api\nimport octobot_trading.personal_data as trading_personal_data\ntry:\n    import tentacles.Services.Services_feeds.trading_view_service_feed as trading_view_service_feed\nexcept ImportError:\n    if commons_constants.USE_MINIMAL_LIBS:\n        # mock trading_view_service_feed imports\n        class TradingViewServiceFeedImportMock:\n            class TradingViewServiceFeed:\n                def get_name(self, *args, **kwargs):\n                    raise ImportError(\"trading_view_service_feed not installed\")\n    trading_view_service_feed = TradingViewServiceFeedImportMock()\nimport tentacles.Trading.Mode.daily_trading_mode.daily_trading as daily_trading_mode\nimport octobot_trading.constants as trading_constants\nimport octobot_trading.enums as trading_enums\nimport octobot_trading.exchanges as trading_exchanges\nimport octobot_trading.modes as trading_modes\nimport octobot_trading.errors as trading_errors\nimport octobot_trading.modes.script_keywords as script_keywords\n\n\n_CANCEL_POLICIES_CACHE = {}\n\n\nclass TradingViewSignalsTradingMode(trading_modes.AbstractTradingMode):\n    SERVICE_FEED_CLASS = trading_view_service_feed.TradingViewServiceFeed if hasattr(trading_view_service_feed, 'TradingViewServiceFeed') else None\n    TRADINGVIEW_FUTURES_SUFFIXES = [\".P\"]\n    PARAM_SEPARATORS = [\";\", \"\\\\n\", \"\\n\"]\n\n    EXCHANGE_KEY = \"EXCHANGE\"\n    TRADING_TYPE_KEY = \"TRADING_TYPE\"   # expect a trading_enums.ExchangeTypes value\n    SYMBOL_KEY = \"SYMBOL\"\n    SIGNAL_KEY = \"SIGNAL\"\n    PRICE_KEY = \"PRICE\"\n    VOLUME_KEY = \"VOLUME\"\n    REDUCE_ONLY_KEY = \"REDUCE_ONLY\"\n    ORDER_TYPE_SIGNAL = \"ORDER_TYPE\"\n    STOP_PRICE_KEY = \"STOP_PRICE\"\n    TAG_KEY = \"TAG\"\n    EXCHANGE_ORDER_IDS = \"EXCHANGE_ORDER_IDS\"\n    LEVERAGE = \"LEVERAGE\"\n    TAKE_PROFIT_PRICE_KEY = \"TAKE_PROFIT_PRICE\"\n    TAKE_PROFIT_VOLUME_RATIO_KEY = \"TAKE_PROFIT_VOLUME_RATIO\"\n    ALLOW_HOLDINGS_ADAPTATION_KEY = \"ALLOW_HOLDINGS_ADAPTATION\"\n    TRAILING_PROFILE = \"TRAILING_PROFILE\"\n    CANCEL_POLICY = \"CANCEL_POLICY\"\n    CANCEL_POLICY_PARAMS = \"CANCEL_POLICY_PARAMS\"\n    PARAM_PREFIX_KEY = \"PARAM_\"\n    BUY_SIGNAL = \"buy\"\n    SELL_SIGNAL = \"sell\"\n    MARKET_SIGNAL = \"market\"\n    LIMIT_SIGNAL = \"limit\"\n    STOP_SIGNAL = \"stop\"\n    CANCEL_SIGNAL = \"cancel\"\n    SIDE_PARAM_KEY = \"SIDE\"\n\n    def __init__(self, config, exchange_manager):\n        super().__init__(config, exchange_manager)\n        self.USE_MARKET_ORDERS = True\n        self.CANCEL_PREVIOUS_ORDERS = True\n        self.merged_simple_symbol = None\n        self.str_symbol = None\n\n    def init_user_inputs(self, inputs: dict) -> None:\n        \"\"\"\n        Called right before starting the tentacle, should define all the tentacle's user inputs unless\n        those are defined somewhere else.\n        \"\"\"\n        self.UI.user_input(\n            \"use_maximum_size_orders\", commons_enums.UserInputTypes.BOOLEAN, False, inputs,\n            title=\"All in trades: Trade with all available funds at each order.\",\n        )\n        self.USE_MARKET_ORDERS = self.UI.user_input(\n            \"use_market_orders\", commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=\"Use market orders: If enabled, placed orders will be market orders only. Otherwise order prices \"\n                  \"are set using the Fixed limit prices difference value.\",\n        )\n        self.UI.user_input(\n            \"close_to_current_price_difference\", commons_enums.UserInputTypes.FLOAT, 0.005, inputs,\n            min_val=0,\n            title=\"Fixed limit prices difference: Difference to take into account when placing a limit order \"\n                  \"(used if fixed limit prices is enabled). For a 200 USD price and 0.005 in difference: \"\n                  \"buy price would be 199 and sell price 201.\",\n        )\n        self.CANCEL_PREVIOUS_ORDERS = self.UI.user_input(\n            \"cancel_previous_orders\", commons_enums.UserInputTypes.BOOLEAN, True, inputs,\n            title=\"Cancel previous orders: If enabled, cancel other orders associated to the same symbol when \"\n                  \"receiving a signal. This way, only the latest signal will be taken into account.\",\n        )\n\n    @classmethod\n    def get_supported_exchange_types(cls) -> list:\n        \"\"\"\n        :return: The list of supported exchange types\n        \"\"\"\n        return [\n            trading_enums.ExchangeTypes.SPOT,\n            trading_enums.ExchangeTypes.FUTURE,\n        ]\n\n    def get_current_state(self) -> (str, float):\n        return super().get_current_state()[0] if self.producers[0].state is None else self.producers[0].state.name, \\\n               self.producers[0].final_eval\n\n    def get_mode_producer_classes(self) -> list:\n        return [TradingViewSignalsModeProducer]\n\n    def get_mode_consumer_classes(self) -> list:\n        return [TradingViewSignalsModeConsumer]\n\n    async def _get_feed_consumers(self):\n        parsed_symbol = symbol_util.parse_symbol(self.symbol)\n        self.str_symbol = str(parsed_symbol)\n        self.merged_simple_symbol = parsed_symbol.merged_str_base_and_quote_only_symbol(market_separator=\"\")\n        feed_consumer = []\n        if self.SERVICE_FEED_CLASS is None:\n            if commons_constants.USE_MINIMAL_LIBS:\n                self.logger.debug(\n                    \"Trading view service feed not installed, this trading mode won't be listening to trading view signals.\"\n                )\n            else:\n                raise ImportError(\"TradingViewServiceFeed not installed\")\n        else:\n            service_feed = services_api.get_service_feed(self.SERVICE_FEED_CLASS, self.bot_id)\n            if service_feed is not None:\n                feed_consumer = [await channels.get_chan(service_feed.FEED_CHANNEL.get_name()).new_consumer(\n                    self._trading_view_signal_callback\n                )]\n            else:\n                self.logger.error(\"Impossible to find the Trading view service feed, this trading mode can't work.\")\n        return feed_consumer\n\n    async def create_consumers(self) -> list:\n        consumers = await super().create_consumers()\n        return consumers + await self._get_feed_consumers()\n\n    @classmethod\n    def _adapt_symbol(cls, parsed_data):\n        if cls.SYMBOL_KEY not in parsed_data:\n            return\n        symbol = parsed_data[cls.SYMBOL_KEY]\n        for suffix in cls.TRADINGVIEW_FUTURES_SUFFIXES:\n            if symbol.endswith(suffix):\n                parsed_data[cls.SYMBOL_KEY] = symbol.split(suffix)[0]\n                return\n\n    @classmethod\n    def parse_signal_data(cls, signal_data: str, errors: list) -> dict:\n        if isinstance(signal_data, dict):\n            # already parsed: return a deep copy to avoid modifying the original data\n            return copy.deepcopy(signal_data)\n        parsed_data = {}\n        # replace all split char by a single one\n        splittable_data = signal_data\n        final_split_char = cls.PARAM_SEPARATORS[0]\n        for split_char in cls.PARAM_SEPARATORS[1:]:\n            splittable_data = splittable_data.replace(split_char, final_split_char)\n        for line in splittable_data.split(final_split_char):\n            if not line.strip():\n                # ignore empty lines\n                continue\n            values = line.split(\"=\")\n            try:\n                value = values[1].strip()\n                # restore booleans\n                lower_val = value.lower()\n                if lower_val in (\"true\", \"false\"):\n                    value = lower_val == \"true\"\n                parsed_data[values[0].strip()] = value\n            except IndexError:\n                errors.append(f\"Invalid signal line in trading view signal, ignoring it. Line: \\\"{line}\\\"\")\n\n        cls._adapt_symbol(parsed_data)\n        return parsed_data\n\n\n    @classmethod\n    def is_compatible_trading_type(cls, parsed_signal: dict, trading_type: trading_enums.ExchangeTypes) -> bool:\n        if parsed_trading_type := parsed_signal.get(cls.TRADING_TYPE_KEY):\n            return parsed_trading_type == trading_type.value\n        return True\n\n    def _log_error_message_if_relevant(self, parsed_data: dict, signal_data: str):\n        # only log error messages on one TradingViewSignalsTradingMode instance to avoid logging errors multiple times\n        if self.is_first_trading_mode_on_this_matrix():\n            all_trading_modes = trading_modes.get_trading_modes_of_this_type_on_this_matrix(self)\n            # Can log error message: this is the first trading mode on this matrix. \n            # Each is notified by signals and only this one will log errors to avoid duplicating logs\n            if not any(\n                trading_mode.is_relevant_signal(parsed_data)\n                for trading_mode in all_trading_modes\n            ):\n                # only log error if the signal is not relevant to any other trading mode on this matrix\n                enabled_exchanges = set()\n                enabled_symbols = set()\n                for trading_mode in all_trading_modes:\n                    enabled_exchanges.add(trading_mode.exchange_manager.exchange_name)\n                    enabled_symbols.add(f\"{trading_mode.str_symbol} (or {self.merged_simple_symbol})\")\n                self.logger.error(\n                    f\"Ignored TradingView alert - unrelated to profile exchanges: {', '.join(enabled_exchanges)} and symbols: {', '.join(enabled_symbols)} (alert: {signal_data})\"\n                )\n\n    def is_relevant_signal(self, parsed_data: dict) -> bool:\n        if not self.is_compatible_trading_type(parsed_data, trading_exchanges.get_exchange_type(self.exchange_manager)):\n            return False\n        elif parsed_data[self.EXCHANGE_KEY].lower() not in self.exchange_manager.exchange_name:\n            return False\n        elif parsed_data[self.SYMBOL_KEY] not in (self.merged_simple_symbol, self.str_symbol):\n            return False\n        return True\n\n    async def _trading_view_signal_callback(self, data):\n        signal_data = data.get(\"metadata\", \"\")\n        errors = []\n        parsed_data = self.parse_signal_data(signal_data, errors)\n        for error in errors:\n            self.logger.error(error)\n        try:\n            if self.is_relevant_signal(parsed_data):\n                await self.producers[0].signal_callback(parsed_data, script_keywords.get_base_context(self))\n            else:\n                self._log_error_message_if_relevant(parsed_data, signal_data)\n        except (trading_errors.InvalidArgumentError, trading_errors.InvalidCancelPolicyError) as e:\n            self.logger.error(f\"Error when processing trading view signal: {e} (signal: {signal_data})\")\n        except trading_errors.MissingFunds as e:\n            self.logger.error(f\"Error when processing trading view signal: not enough funds: {e} (signal: {signal_data})\")\n        except KeyError as e:\n            self.logger.error(f\"Error when processing trading view signal: missing {e} required value (signal: {signal_data})\")\n        except Exception as e:\n            self.logger.error(\n                f\"Unexpected error when processing trading view signal: {e} {e.__class__.__name__} (signal: {signal_data})\"\n            )\n\n    @classmethod\n    def get_is_symbol_wildcard(cls) -> bool:\n        return False\n\n    @staticmethod\n    def is_backtestable():\n        return False\n\n\nclass TradingViewSignalsModeConsumer(daily_trading_mode.DailyTradingModeConsumer):\n    def __init__(self, trading_mode):\n        super().__init__(trading_mode)\n        self.QUANTITY_MIN_PERCENT = decimal.Decimal(str(0.1))\n        self.QUANTITY_MAX_PERCENT = decimal.Decimal(str(0.9))\n\n        self.QUANTITY_MARKET_MIN_PERCENT = decimal.Decimal(str(0.5))\n        self.QUANTITY_MARKET_MAX_PERCENT = trading_constants.ONE\n        self.QUANTITY_BUY_MARKET_ATTENUATION = decimal.Decimal(str(0.2))\n\n        self.BUY_LIMIT_ORDER_MAX_PERCENT = decimal.Decimal(str(0.995))\n        self.BUY_LIMIT_ORDER_MIN_PERCENT = decimal.Decimal(str(0.99))\n\n        self.USE_CLOSE_TO_CURRENT_PRICE = True\n        self.CLOSE_TO_CURRENT_PRICE_DEFAULT_RATIO = decimal.Decimal(str(trading_mode.trading_config.get(\"close_to_current_price_difference\",\n                                                                                    0.02)))\n        self.BUY_WITH_MAXIMUM_SIZE_ORDERS = trading_mode.trading_config.get(\"use_maximum_size_orders\", False)\n        self.SELL_WITH_MAXIMUM_SIZE_ORDERS = trading_mode.trading_config.get(\"use_maximum_size_orders\", False)\n        self.USE_STOP_ORDERS = False\n\n\nclass TradingViewSignalsModeProducer(daily_trading_mode.DailyTradingModeProducer):\n    def __init__(self, channel, config, trading_mode, exchange_manager):\n        super().__init__(channel, config, trading_mode, exchange_manager)\n        self.EVAL_BY_STATES = {\n            trading_enums.EvaluatorStates.LONG: -0.6,\n            trading_enums.EvaluatorStates.SHORT: 0.6,\n            trading_enums.EvaluatorStates.VERY_LONG: -1,\n            trading_enums.EvaluatorStates.VERY_SHORT: 1,\n            trading_enums.EvaluatorStates.NEUTRAL: 0,\n        }\n\n    def get_channels_registration(self):\n        # do not register on matrix or candles channels\n        return []\n\n    async def set_final_eval(self, matrix_id: str, cryptocurrency: str, symbol: str, time_frame, trigger_source: str):\n        # Ignore matrix calls\n        pass\n\n    def _parse_pre_update_order_details(self, parsed_data):\n        return {\n            TradingViewSignalsModeConsumer.LEVERAGE:\n                parsed_data.get(TradingViewSignalsTradingMode.LEVERAGE, None),\n        }\n\n    async def _parse_order_details(self, ctx, parsed_data):\n        side = parsed_data[TradingViewSignalsTradingMode.SIGNAL_KEY].casefold()\n        order_type = parsed_data.get(TradingViewSignalsTradingMode.ORDER_TYPE_SIGNAL, \"\").casefold()\n        order_exchange_creation_params = {\n            param_name.split(TradingViewSignalsTradingMode.PARAM_PREFIX_KEY)[1]: param_value\n            for param_name, param_value in parsed_data.items()\n            if param_name.startswith(TradingViewSignalsTradingMode.PARAM_PREFIX_KEY)\n        }\n        parsed_side = None\n        if side == TradingViewSignalsTradingMode.SELL_SIGNAL:\n            parsed_side = trading_enums.TradeOrderSide.SELL.value\n            if order_type == TradingViewSignalsTradingMode.MARKET_SIGNAL:\n                state = trading_enums.EvaluatorStates.VERY_SHORT\n            elif order_type in (TradingViewSignalsTradingMode.LIMIT_SIGNAL, TradingViewSignalsTradingMode.STOP_SIGNAL):\n                state = trading_enums.EvaluatorStates.SHORT\n            else:\n                state = trading_enums.EvaluatorStates.VERY_SHORT if self.trading_mode.USE_MARKET_ORDERS \\\n                    else trading_enums.EvaluatorStates.SHORT\n        elif side == TradingViewSignalsTradingMode.BUY_SIGNAL:\n            parsed_side = trading_enums.TradeOrderSide.BUY.value\n            if order_type == TradingViewSignalsTradingMode.MARKET_SIGNAL:\n                state = trading_enums.EvaluatorStates.VERY_LONG\n            elif order_type in (TradingViewSignalsTradingMode.LIMIT_SIGNAL, TradingViewSignalsTradingMode.STOP_SIGNAL):\n                state = trading_enums.EvaluatorStates.LONG\n            else:\n                state = trading_enums.EvaluatorStates.VERY_LONG if self.trading_mode.USE_MARKET_ORDERS \\\n                    else trading_enums.EvaluatorStates.LONG\n        elif side == TradingViewSignalsTradingMode.CANCEL_SIGNAL:\n            state = trading_enums.EvaluatorStates.NEUTRAL\n        else:\n            raise trading_errors.InvalidArgumentError(\n                f\"Unknown signal: {parsed_data[TradingViewSignalsTradingMode.SIGNAL_KEY]}, full data= {parsed_data}\"\n            )\n        target_price = 0 if order_type == TradingViewSignalsTradingMode.MARKET_SIGNAL else (\n            await self._parse_element(ctx, parsed_data, TradingViewSignalsTradingMode.PRICE_KEY, 0, True))\n        stop_price = await self._parse_element(\n            ctx, parsed_data, TradingViewSignalsTradingMode.STOP_PRICE_KEY, math.nan, True\n        )\n        tp_price = await self._parse_element(\n            ctx, parsed_data, TradingViewSignalsTradingMode.TAKE_PROFIT_PRICE_KEY, math.nan, True\n        )\n        additional_tp_volume_ratios = []\n        if first_volume := await self._parse_element(\n            ctx, parsed_data, TradingViewSignalsTradingMode.TAKE_PROFIT_VOLUME_RATIO_KEY, 0, False\n        ):\n            additional_tp_volume_ratios.append(first_volume)\n        additional_tp_prices = await self._parse_additional_decimal_elements(\n            ctx, parsed_data, f\"{TradingViewSignalsTradingMode.TAKE_PROFIT_PRICE_KEY}_\", math.nan, True\n        )\n        additional_tp_volume_ratios += await self._parse_additional_decimal_elements(\n            ctx, parsed_data, f\"{TradingViewSignalsTradingMode.TAKE_PROFIT_VOLUME_RATIO_KEY}_\", 0, False\n        )\n        allow_holdings_adaptation = parsed_data.get(TradingViewSignalsTradingMode.ALLOW_HOLDINGS_ADAPTATION_KEY, False)\n        reduce_only = parsed_data.get(TradingViewSignalsTradingMode.REDUCE_ONLY_KEY, False)\n        amount = await self._parse_volume(\n            ctx, parsed_data, parsed_side, target_price, allow_holdings_adaptation, reduce_only\n        )\n        trailing_profile = parsed_data.get(TradingViewSignalsTradingMode.TRAILING_PROFILE)\n        maybe_cancel_policy, cancel_policy_params = self._parse_cancel_policy(parsed_data)\n        order_data = {\n            TradingViewSignalsModeConsumer.PRICE_KEY: target_price,\n            TradingViewSignalsModeConsumer.VOLUME_KEY: amount,\n            TradingViewSignalsModeConsumer.STOP_PRICE_KEY: stop_price,\n            TradingViewSignalsModeConsumer.STOP_ONLY: order_type == TradingViewSignalsTradingMode.STOP_SIGNAL,\n            TradingViewSignalsModeConsumer.TAKE_PROFIT_PRICE_KEY: tp_price,\n            TradingViewSignalsModeConsumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: additional_tp_prices,\n            TradingViewSignalsModeConsumer.ADDITIONAL_TAKE_PROFIT_VOLUME_RATIOS_KEY: additional_tp_volume_ratios,\n            TradingViewSignalsModeConsumer.REDUCE_ONLY_KEY: reduce_only,\n            TradingViewSignalsModeConsumer.TAG_KEY:\n                parsed_data.get(TradingViewSignalsTradingMode.TAG_KEY, None),\n            TradingViewSignalsModeConsumer.TRAILING_PROFILE: trailing_profile.casefold() if trailing_profile else None,\n            TradingViewSignalsModeConsumer.CANCEL_POLICY: maybe_cancel_policy,\n            TradingViewSignalsModeConsumer.CANCEL_POLICY_PARAMS: cancel_policy_params,\n            TradingViewSignalsModeConsumer.EXCHANGE_ORDER_IDS:\n                parsed_data.get(TradingViewSignalsTradingMode.EXCHANGE_ORDER_IDS, None),\n            TradingViewSignalsModeConsumer.LEVERAGE:\n                parsed_data.get(TradingViewSignalsTradingMode.LEVERAGE, None),\n            TradingViewSignalsModeConsumer.ORDER_EXCHANGE_CREATION_PARAMS: order_exchange_creation_params,\n        }\n        return state, order_data\n\n    def _parse_cancel_policy(self, parsed_data):\n        if policy := parsed_data.get(TradingViewSignalsTradingMode.CANCEL_POLICY, None):\n            lowercase_policy = policy.casefold()\n            if not _CANCEL_POLICIES_CACHE:\n                _CANCEL_POLICIES_CACHE.update({\n                    policy.__name__.casefold(): policy.__name__\n                    for policy in tentacles_management.get_all_classes_from_parent(trading_personal_data.OrderCancelPolicy)\n                })\n            try:\n                policy_class = _CANCEL_POLICIES_CACHE[lowercase_policy]\n                policy_params = parsed_data.get(TradingViewSignalsTradingMode.CANCEL_POLICY_PARAMS)\n                parsed_policy_params = json.loads(policy_params.replace(\"'\", '\"')) if isinstance(policy_params, str) else policy_params\n                return policy_class, parsed_policy_params\n            except KeyError:\n                raise trading_errors.InvalidCancelPolicyError(\n                    f\"Unknown cancel policy: {policy}. Available policies: {', '.join(_CANCEL_POLICIES_CACHE.keys())}\"\n                )\n\n        return None, None\n\n    async def _parse_additional_decimal_elements(self, ctx, parsed_data, element_prefix, default, is_price):\n        values: list[decimal.Decimal] = []\n        for key, value in parsed_data.items():\n            if key.startswith(element_prefix) and len(key.split(element_prefix)) == 2:\n                values.append(await self._parse_element(ctx, parsed_data, key, default, is_price))\n        return values\n\n    async def _parse_element(self, ctx, parsed_data, key, default, is_price)-> decimal.Decimal:\n        target_value = decimal.Decimal(str(default))\n        value = parsed_data.get(key, 0)\n        if is_price:\n            if input_price_or_offset := value:\n                target_value = await script_keywords.get_price_with_offset(\n                    ctx, input_price_or_offset, use_delta_type_as_flat_value=True\n                )\n        else:\n            target_value = decimal.Decimal(str(value))\n        return target_value\n\n    async def _parse_volume(self, ctx, parsed_data, side, target_price, allow_holdings_adaptation, reduce_only):\n        user_volume = str(parsed_data.get(TradingViewSignalsTradingMode.VOLUME_KEY, 0))\n        if user_volume == \"0\":\n            return trading_constants.ZERO\n        return await script_keywords.get_amount_from_input_amount(\n            context=ctx,\n            input_amount=user_volume,\n            side=side,\n            reduce_only=reduce_only,\n            is_stop_order=False,\n            use_total_holding=False,\n            target_price=target_price,\n            # raise when not enough funds to create an order according to user input\n            allow_holdings_adaptation=allow_holdings_adaptation,\n        )\n\n    async def signal_callback(self, parsed_data: dict, ctx):\n        _, dependencies = await self.apply_cancel_policies()\n        if self.trading_mode.CANCEL_PREVIOUS_ORDERS:\n            # cancel open orders\n            _, new_dependencies = await self.cancel_symbol_open_orders(self.trading_mode.symbol)\n            if new_dependencies:\n                if dependencies:\n                    dependencies.extend(new_dependencies)\n                else:\n                    dependencies = new_dependencies\n        pre_update_data = self._parse_pre_update_order_details(parsed_data)\n        await self._process_pre_state_update_actions(ctx, pre_update_data)\n        state, order_data = await self._parse_order_details(ctx, parsed_data)\n        self.final_eval = self.EVAL_BY_STATES[state]\n        # Use daily trading mode state system\n        await self._set_state(\n            self.trading_mode.cryptocurrency, ctx.symbol, state, order_data, dependencies=dependencies\n        )\n\n    async def _process_pre_state_update_actions(self, context, data: dict):\n        try:\n            if leverage := data.get(TradingViewSignalsModeConsumer.LEVERAGE):\n                await self.trading_mode.set_leverage(context.symbol, None, decimal.Decimal(str(leverage)))\n        except Exception as err:\n            self.logger.exception(\n                err, True, f\"Error when processing pre_state_update_actions: {err} (data: {data})\"\n            )\n\n    async def _set_state(\n        self, cryptocurrency: str, symbol: str, new_state, order_data, \n        dependencies: typing.Optional[commons_signals.SignalDependencies] = None\n    ):\n        async with self.trading_mode_trigger():\n            self.state = new_state\n            self.logger.info(f\"[{symbol}] new state: {self.state.name}\")\n\n            # if new state is not neutral --> cancel orders and create new else keep orders\n            if new_state is not trading_enums.EvaluatorStates.NEUTRAL:\n                # call orders creation from consumers\n                await self.submit_trading_evaluation(cryptocurrency=cryptocurrency,\n                                                     symbol=symbol,\n                                                     time_frame=None,\n                                                     final_note=self.final_eval,\n                                                     state=self.state,\n                                                     data=order_data,\n                                                     dependencies=dependencies)\n\n                # send_notification\n                if not self.exchange_manager.is_backtesting:\n                    await self._send_alert_notification(symbol, new_state)\n            else:\n                await self.cancel_orders_from_order_data(symbol, order_data)\n\n    async def cancel_orders_from_order_data(self, symbol: str, order_data) -> tuple[bool, typing.Optional[commons_signals.SignalDependencies]]:\n        if not self.trading_mode.consumers:\n            return False, None\n\n        exchange_ids = order_data.get(TradingViewSignalsModeConsumer.EXCHANGE_ORDER_IDS, None)\n        cancel_order_raw_side = order_data.get(\n            TradingViewSignalsModeConsumer.ORDER_EXCHANGE_CREATION_PARAMS, {}).get(\n                TradingViewSignalsTradingMode.SIDE_PARAM_KEY, None)\n        cancel_order_side = trading_enums.TradeOrderSide.BUY if cancel_order_raw_side == trading_enums.TradeOrderSide.BUY.value \\\n            else trading_enums.TradeOrderSide.SELL if cancel_order_raw_side == trading_enums.TradeOrderSide.SELL.value else None\n        cancel_order_tag = order_data.get(TradingViewSignalsModeConsumer.TAG_KEY, None)\n\n        # cancel open orders\n        return await self.cancel_symbol_open_orders(\n            symbol, side=cancel_order_side, tag=cancel_order_tag, exchange_order_ids=exchange_ids\n        )\n"
  },
  {
    "path": "metadata.yaml",
    "content": "author: DrakkarSoftware\nname: base\nshort-name: base\nrepository: OctoBot-Tentacles\nversion: VERSION_PLACEHOLDER\ndescription: >\n  This package contains default evaluators, strategies, utilitary modules, interfaces and trading modes.\ntags:\n  - officials\n"
  },
  {
    "path": "octobot_config.json",
    "content": "{\n  \"backtesting\": {\n      \"enabled\": false,\n      \"files\": []\n  },\n  \"crypto-currencies\":{\n    \"Bitcoin\": {\n      \"pairs\" : [\"BTC/USDT\"]\n    }\n  },\n  \"exchanges\": {\n    \"binance\": {\n      \"api-key\": \"your-api-key-here\",\n      \"api-secret\": \"your-api-secret-here\"\n    }\n  },\n  \"services\": {},\n  \"notification\":{\n    \"global-info\": true,\n    \"price-alerts\": true,\n    \"trades\": true,\n    \"notification-type\": []\n  },\n  \"trading\":{\n    \"risk\": 0.5,\n    \"reference-market\": \"BTC\"\n  },\n  \"trader\":{\n    \"enabled\": false\n  },\n  \"trader-simulator\":{\n    \"enabled\": true,\n    \"fees\": {\n        \"maker\": 0.1,\n        \"taker\": 0.1\n    },\n    \"starting-portfolio\": {\n      \"BTC\": 10,\n      \"USDT\": 1000\n    }\n  }\n}\n"
  },
  {
    "path": "profiles/arbitrage_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            },\n            \"coinbase\": {\n                \"enabled\": true\n            },\n            \"kucoin\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 3,\n        \"description\": \"ArbitrageTrading is watching prices of the configured trading pairs across the available exchanges to find arbitrage opportunities.\",\n        \"id\": \"arbitrage_trading\",\n        \"name\": \"Arbitrage Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/arbitrage_trading/specific_config/ArbitrageTradingMode.json",
    "content": "{\n    \"exchanges_to_trade_on\": [],\n    \"minimal_price_delta_percent\": 0.35,\n    \"portfolio_percent_per_trade\": 25,\n    \"required_strategies\": [],\n    \"stop_loss_delta_percent\": 0.1\n}"
  },
  {
    "path": "profiles/arbitrage_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"ArbitrageTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/copy_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 1,\n        \"description\": \"Copy Trading is following moves from the specified trading strategy.\",\n        \"id\": \"copy_trading\",\n        \"name\": \"Copy Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/copy_trading/specific_config/RemoteTradingSignalsTradingMode.json",
    "content": "{\n    \"trading_strategy\": \"trading_strategy_identifier\",\n    \"required_strategies\": []\n}"
  },
  {
    "path": "profiles/copy_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"RemoteTradingSignalsTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/daily_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 3,\n        \"complexity\": 1,\n        \"description\": \"DailyTrading is a versatile profile that catches buy and sell opportunities using RSI and moving average indicators.\",\n        \"id\": \"daily_trading\",\n        \"name\": \"DailyTrading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/daily_trading/specific_config/DailyTradingMode.json",
    "content": "{\n    \"close_to_current_price_difference\": 0.005,\n    \"default_config\": [\n        \"SimpleStrategyEvaluator\"\n    ],\n    \"required_strategies\": [\n        \"SimpleStrategyEvaluator\",\n        \"TechnicalAnalysisStrategyEvaluator\"\n    ],\n    \"required_strategies_min_count\": 1,\n    \"sell_with_maximum_size_orders\": false,\n    \"buy_with_maximum_size_orders\": false,\n    \"use_prices_close_to_current_price\": false,\n    \"disable_buy_orders\": false,\n    \"disable_sell_orders\": false,\n    \"use_stop_orders\": true\n}"
  },
  {
    "path": "profiles/daily_trading/specific_config/SimpleStrategyEvaluator.json",
    "content": "{\n    \"default_config\": [\n        \"DoubleMovingAverageTrendEvaluator\",\n        \"RSIMomentumEvaluator\"\n    ],\n    \"required_evaluators\": [\n        \"*\"\n    ],\n    \"required_time_frames\": [\n        \"1h\",\n        \"4h\",\n        \"1d\"\n    ],\n    \"social_evaluators_notification_timeout\": 3600,\n    \"re_evaluate_TA_when_social_or_realtime_notification\": true,\n    \"background_social_evaluators\": [\n      \"RedditForumEvaluator\"\n    ]\n}"
  },
  {
    "path": "profiles/daily_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Evaluator\": {\n            \"DoubleMovingAverageTrendEvaluator\": true,\n            \"RSIMomentumEvaluator\": true,\n            \"SimpleStrategyEvaluator\": true\n        },\n        \"Trading\": {\n            \"DailyTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/dip_analyser/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            },\n            \"Ethereum\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"ETH/BTC\",\n                    \"ETH/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 2,\n        \"complexity\": 1,\n        \"description\": \"DipAnalyser is a profile adapted to volatile markets. It will look for local market bottoms, weight them and buy these bottoms. It never sells except after a buy order is filled.\",\n        \"id\": \"dip_analyser\",\n        \"name\": \"Dip Analyser\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/dip_analyser/specific_config/DipAnalyserStrategyEvaluator.json",
    "content": "{\n    \"default_config\": [\n        \"KlingerOscillatorReversalConfirmationMomentumEvaluator\",\n        \"RSIWeightMomentumEvaluator\"\n    ],\n    \"required_evaluators\": [\n        \"InstantFluctuationsEvaluator\",\n        \"KlingerOscillatorReversalConfirmationMomentumEvaluator\",\n        \"RSIWeightMomentumEvaluator\"\n    ],\n    \"required_time_frames\": [\n        \"4h\"\n    ]\n}"
  },
  {
    "path": "profiles/dip_analyser/specific_config/DipAnalyserTradingMode.json",
    "content": "{\n    \"required_strategies\": [\n        \"DipAnalyserStrategyEvaluator\"\n    ],\n    \"sell_orders_count\": 3,\n    \"light_weight_price_multiplier\": 1.04,\n    \"medium_weight_price_multiplier\": 1.07,\n    \"heavy_weight_price_multiplier\": 1.1,\n    \"light_weight_volume_multiplier\": 0.5,\n    \"medium_weight_volume_multiplier\": 0.7,\n    \"heavy_weight_volume_multiplier\": 1\n}"
  },
  {
    "path": "profiles/dip_analyser/specific_config/InstantFluctuationsEvaluator.json",
    "content": "{\n    \"price_difference_threshold_percent\": 1,\n    \"volume_difference_threshold_percent\": 400,\n    \"time_frame\": \"1m\"\n}"
  },
  {
    "path": "profiles/dip_analyser/specific_config/RSIWeightMomentumEvaluator.json",
    "content": "{\n  \"period\": 14,\n  \"slow_eval_count\": 16,\n  \"fast_eval_count\": 4,\n  \"RSI_to_weight\": [\n    {\n      \"slow_threshold\": 30,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 20,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 2\n              }\n          },\n          {\n            \"fast_threshold\" : 30,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 35,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 20,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 3\n              }\n          },\n          {\n            \"fast_threshold\" : 35,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 45,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 20,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 3\n              }\n          },\n          {\n            \"fast_threshold\" : 40,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 55,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 45,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 65,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 45,\n            \"weights\": {\n                \"price\": 1,\n                \"volume\": 1\n              }\n          },\n          {\n            \"fast_threshold\" : 55,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 2\n              }\n          },\n          {\n            \"fast_threshold\" : 60,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 1\n              }\n          }\n        ]\n    },\n    {\n      \"slow_threshold\": 70,\n      \"fast_thresholds\":\n        [\n          {\n            \"fast_threshold\" : 55,\n            \"weights\": {\n                \"price\": 3,\n                \"volume\": 2\n              }\n          },\n          {\n            \"fast_threshold\" : 70,\n            \"weights\": {\n                \"price\": 2,\n                \"volume\": 2\n              }\n          }\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "profiles/dip_analyser/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Evaluator\": {\n            \"DipAnalyserStrategyEvaluator\": true,\n            \"InstantFluctuationsEvaluator\": true,\n            \"KlingerOscillatorReversalConfirmationMomentumEvaluator\": true,\n            \"RSIWeightMomentumEvaluator\": true\n        },\n        \"Trading\": {\n            \"DipAnalyserTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/gpt_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true,\n                \"exchange-type\": \"spot\"\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": true\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 1\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"ChatGPT_logo.svg\",\n        \"complexity\": 2,\n        \"description\": \"GPT Smart DCA uses ChatGPT to predict the market. It can be used to trade directly based on the profile's DCA Trading mode configuration.\\nConfigure the GPTEvaluator to customize the way market data are sent to ChatGPT and the DCATradingMode to change how entries and exits should be created.\",\n        \"id\": \"gpt_trading\",\n        \"imported\": false,\n        \"name\": \"GPT Trading\",\n        \"origin_url\": null,\n        \"read_only\": true,\n        \"risk\": 3\n    }\n}"
  },
  {
    "path": "profiles/gpt_trading/specific_config/DCATradingMode.json",
    "content": "{\n    \"buy_order_amount\": \"8%t\",\n    \"cancel_open_orders_at_each_entry\": true,\n    \"default_config\": [\n        \"SimpleStrategyEvaluator\"\n    ],\n    \"entry_limit_orders_price_percent\": 1.3,\n    \"exit_limit_orders_price_percent\": 2,\n    \"minutes_before_next_buy\": 10080,\n    \"required_strategies\": [\n        \"SimpleStrategyEvaluator\",\n        \"TechnicalAnalysisStrategyEvaluator\"\n    ],\n    \"secondary_entry_orders_amount\": \"8%t\",\n    \"secondary_entry_orders_count\": 1,\n    \"secondary_entry_orders_price_percent\": 1.3,\n    \"secondary_exit_orders_count\": 1,\n    \"secondary_exit_orders_price_percent\": 0.5,\n    \"trigger_mode\": \"Maximum evaluators signals based\",\n    \"use_init_entry_orders\": false,\n    \"use_market_entry_orders\": false,\n    \"use_secondary_entry_orders\": true,\n    \"use_secondary_exit_orders\": false,\n    \"use_stop_losses\": false,\n    \"use_take_profit_exit_orders\": true\n}"
  },
  {
    "path": "profiles/gpt_trading/specific_config/GPTEvaluator.json",
    "content": "{\n    \"GPT_model\": \"gpt-3.5-turbo\",\n    \"indicator\": \"No indicator: raw candles price data\",\n    \"max_confidence_threshold\": 60,\n    \"period\": 20,\n    \"source\": \"Full candle (For no indicator only)\",\n    \"allow_reevaluation\": false,\n    \"max_gpt_tokens\": -1\n}"
  },
  {
    "path": "profiles/gpt_trading/specific_config/SimpleStrategyEvaluator.json",
    "content": "{\n    \"background_social_evaluators\": [\n        \"RedditForumEvaluator\"\n    ],\n    \"default_config\": [\n        \"DoubleMovingAverageTrendEvaluator\",\n        \"RSIMomentumEvaluator\"\n    ],\n    \"re_evaluate_TA_when_social_or_realtime_notification\": true,\n    \"required_candles_count\": 1000,\n    \"required_evaluators\": [\n        \"*\"\n    ],\n    \"required_time_frames\": [\n        \"4h\"\n    ],\n    \"social_evaluators_notification_timeout\": 3600\n}"
  },
  {
    "path": "profiles/gpt_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Evaluator\": {\n            \"GPTEvaluator\": true,\n            \"SimpleStrategyEvaluator\": true\n        },\n        \"Trading\": {\n            \"DCATradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/grid_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 1,\n        \"description\": \"GridTrading is a profile configured to create a pre-defined amount of buy and sell orders at fixed intervals to profit from any market move. When an order is filled, a mirror order is instantly created and generates profit when completed. GridTrading is a simpler version of the StaggeredOrdersTradingMode.\",\n        \"id\": \"grid_trading\",\n        \"name\": \"Grid Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/grid_trading/specific_config/GridTradingMode.json",
    "content": "{\n  \"required_strategies\": [],\n  \"pair_settings\": [\n    {\n      \"pair\": \"BTC/USDT\",\n      \"flat_spread\": 2000,\n      \"flat_increment\": 1000,\n      \"buy_orders_count\": 25,\n      \"sell_orders_count\": 25,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"ignore_exchange_fees\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": false,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    },\n    {\n      \"pair\": \"ADA/ETH\",\n      \"flat_spread\": 0.00002,\n      \"flat_increment\": 0.00001,\n      \"buy_orders_count\": 25,\n      \"sell_orders_count\": 25,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"ignore_exchange_fees\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": false,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    },\n    {\n      \"pair\": \"ETH/USDT\",\n      \"flat_spread\": 10,\n      \"flat_increment\": 5,\n      \"buy_orders_count\": 25,\n      \"sell_orders_count\": 25,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"ignore_exchange_fees\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": false,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    }\n  ]\n}"
  },
  {
    "path": "profiles/grid_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"GridTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/index_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            },\n            \"Ethereum\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"ETH/USDT\"\n                ]\n            },\n            \"Solana\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"SOL/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true,\n                \"exchange-type\": \"spot\"\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"auto_update\": false,\n        \"avatar\": \"default_profile.png\",\n        \"complexity\": 1,\n        \"description\": \"Index Trading will maintain a custom crypto index using each enabled traded coin.\",\n        \"extra_backtesting_time_frames\": [],\n        \"id\": \"index_trading\",\n        \"imported\": false,\n        \"name\": \"Index trading\",\n        \"origin_url\": null,\n        \"read_only\": true,\n        \"risk\": 1,\n        \"slug\": \"\",\n        \"type\": \"live\"\n    }\n}"
  },
  {
    "path": "profiles/index_trading/specific_config/IndexTradingMode.json",
    "content": "{\n    \"index_content\": [],\n    \"rebalance_trigger_min_percent\": 5,\n    \"refresh_interval\": 1,\n    \"required_strategies\": []\n}"
  },
  {
    "path": "profiles/index_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"IndexTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/market_making/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDC\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true,\n                \"exchange-type\": \"spot\"\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": true\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 0.001,\n                \"USDC\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDC\",\n            \"risk\": 1.0\n        }\n    },\n    \"profile\": {\n        \"auto_update\": false,\n        \"avatar\": \"default_profile.png\",\n        \"complexity\": 1,\n        \"description\": \"Market making will create and maintain an order book following a customized market making strategy.\",\n        \"extra_backtesting_time_frames\": [],\n        \"id\": \"market_making\",\n        \"imported\": false,\n        \"name\": \"Market making\",\n        \"origin_url\": null,\n        \"read_only\": true,\n        \"risk\": 3,\n        \"slug\": \"\",\n        \"type\": \"live\"\n    }\n}"
  },
  {
    "path": "profiles/market_making/specific_config/MarketMakingTradingMode.json",
    "content": "{\n    \"asks_count\": 3,\n    \"bids_count\": 3,\n    \"max_spread\": 5,\n    \"min_spread\": 2,\n    \"reference_exchange\": \"local\",\n    \"required_strategies\": []\n}"
  },
  {
    "path": "profiles/market_making/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"MarketMakingTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/non-trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": true\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"USDT\": 10000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 1,\n        \"description\": \"Non-Trading is a profile that does not trade. It will not create any order. Use this profile to run OctoBot without creating any order.\",\n        \"id\": \"default\",\n        \"name\": \"Non-Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/non-trading/specific_config/BlankStrategyEvaluator.json",
    "content": "{\n  \"required_time_frames\" : [\"1h\"],\n  \"required_evaluators\" : [\"*\"],\n  \"required_candles_count\" : 200,\n  \"default_config\" : []\n}"
  },
  {
    "path": "profiles/non-trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Evaluator\": {\n            \"BlankStrategyEvaluator\": true\n        },\n        \"Trading\": {\n            \"BlankTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/signal_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            },\n            \"Ethereum\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"ETH/BTC\",\n                    \"ETH/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 3,\n        \"complexity\": 1,\n        \"description\": \"SignalTrading is adapted to liquid and relatively flat markets. It will try to find reversals and trade them.\",\n        \"id\": \"signal_trading\",\n        \"name\": \"Signal Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/signal_trading/specific_config/MoveSignalsStrategyEvaluator.json",
    "content": "{\n  \"required_time_frames\" : [\"30m\", \"1h\", \"4h\"],\n  \"required_evaluators\" : [\"InstantFluctuationsEvaluator\", \"KlingerOscillatorMomentumEvaluator\", \"BBMomentumEvaluator\"],\n  \"default_config\" : [\"KlingerOscillatorMomentumEvaluator\", \"BBMomentumEvaluator\"]\n}"
  },
  {
    "path": "profiles/signal_trading/specific_config/SignalTradingMode.json",
    "content": "{\n    \"close_to_current_price_difference\": 0.02,\n    \"required_strategies\": [\n        \"MoveSignalsStrategyEvaluator\"\n    ],\n    \"use_maximum_size_orders\": false,\n    \"use_prices_close_to_current_price\": false,\n    \"use_stop_orders\": true\n}"
  },
  {
    "path": "profiles/signal_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Evaluator\": {\n            \"KlingerOscillatorMomentumEvaluator\": true,\n            \"MoveSignalsStrategyEvaluator\": true,\n            \"InstantFluctuationsEvaluator\": true,\n            \"BBMomentumEvaluator\": true\n        },\n        \"Trading\": {\n            \"SignalTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/simple_dca/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 1,\n        \"description\": \"Simple DCA (Dollar cost averaging) is a profile that can help you lower the amount you pay for investments and minimize risk. Instead of purchasing investments at a single price point, with dollar cost averaging you buy in smaller amounts at regular intervals, regardless of price. Over the long term, dollar cost averaging can help lower your investment costs and boost your returns.\",\n        \"id\": \"simple_dca\",\n        \"name\": \"Simple DCA\",\n        \"read_only\": true\n    }\n}\n"
  },
  {
    "path": "profiles/simple_dca/specific_config/DCATradingMode.json",
    "content": "{\n    \"buy_order_amount\": \"5%t\",\n    \"default_config\": [],\n    \"required_strategies\": [],\n    \"cancel_open_orders_at_each_entry\": false,\n    \"minutes_before_next_buy\": 10080,\n    \"trigger_mode\": \"Time based\",\n    \"use_market_entry_orders\": true,\n    \"use_secondary_entry_orders\": false,\n    \"use_stop_losses\": false,\n    \"use_take_profit_exit_orders\": false,\n    \"enable_health_check\": false,\n    \"health_check_orphan_funds_threshold\": 15\n}"
  },
  {
    "path": "profiles/simple_dca/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"DCATradingMode\": true\n        }\n    }\n}\n"
  },
  {
    "path": "profiles/smart_dca/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true,\n                \"exchange-type\": \"spot\"\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 0,\n                \"USDT\": 1000,\n                \"ETH\": 0\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"complexity\": 3,\n        \"description\": \"Smart DCA (Dollar cost averaging) is a profile that trades by quickly entering and exiting the market using up to 3 entries each split into 2 exits. Each entry uses 5% of the portfolio and is signaled when the price is bellow its by EMA values.\",\n        \"imported\": false,\n        \"name\": \"Smart DCA\",\n        \"origin_url\": null,\n        \"read_only\": true,\n        \"risk\": 2,\n        \"id\": \"smart_dca\"\n    }\n}"
  },
  {
    "path": "profiles/smart_dca/specific_config/DCATradingMode.json",
    "content": "{\n    \"buy_order_amount\": \"5%t\",\n    \"default_config\": [\n        \"SimpleStrategyEvaluator\"\n    ],\n    \"required_strategies\": [\n        \"SimpleStrategyEvaluator\"\n\t],\n    \"entry_limit_orders_price_percent\": 0.2,\n    \"exit_limit_orders_price_percent\": 0.35,\n    \"minutes_before_next_buy\": 10080,\n    \"secondary_entry_orders_amount\": \"5%t\",\n    \"secondary_entry_orders_count\": 2,\n    \"secondary_entry_orders_price_percent\": 0.5,\n    \"secondary_exit_orders_count\": 1,\n    \"secondary_exit_orders_price_percent\": 0.5,\n    \"trigger_mode\": \"Maximum evaluators signals based\",\n    \"use_market_entry_orders\": false,\n    \"use_secondary_entry_orders\": true,\n    \"use_secondary_exit_orders\": true,\n    \"use_stop_losses\": false,\n    \"use_take_profit_exit_orders\": true,\n    \"enable_health_check\": false,\n    \"health_check_orphan_funds_threshold\": 15\n}"
  },
  {
    "path": "profiles/smart_dca/specific_config/EMAMomentumEvaluator.json",
    "content": "{\n    \"period_length\": 9,\n    \"price_threshold_percent\": 0\n}"
  },
  {
    "path": "profiles/smart_dca/specific_config/SimpleStrategyEvaluator.json",
    "content": "{\n    \"background_social_evaluators\": [\n        \"RedditForumEvaluator\"\n    ],\n    \"default_config\": [\n        \"DoubleMovingAverageTrendEvaluator\",\n        \"RSIMomentumEvaluator\"\n    ],\n    \"re_evaluate_TA_when_social_or_realtime_notification\": true,\n    \"required_candles_count\": 1000,\n    \"required_evaluators\": [\n        \"*\"\n    ],\n    \"required_time_frames\": [\n        \"4h\"\n    ],\n    \"social_evaluators_notification_timeout\": 3600\n}"
  },
  {
    "path": "profiles/smart_dca/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"DCATradingMode\": true\n        },\n        \"Evaluator\": {\n            \"EMAMomentumEvaluator\": true,\n            \"SimpleStrategyEvaluator\": true\n        }\n    }\n}\n"
  },
  {
    "path": "profiles/staggered_orders_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 3,\n        \"description\": \"StaggeredOrdersTrading is a profile configured to create a buy and sell orders at fixed intervals on a specific price range to profit from any market move. When an order is filled, a mirror order is instantly created and generates profit when completed.\",\n        \"id\": \"staggered_orders_trading\",\n        \"name\": \"Staggered Orders Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/staggered_orders_trading/specific_config/StaggeredOrdersTradingMode.json",
    "content": "{\n  \"required_strategies\": [],\n  \"pair_settings\": [\n    {\n    \"pair\": \"BTC/USDT\",\n    \"mode\": \"mountain\",\n    \"spread_percent\": 6,\n    \"increment_percent\": 3,\n    \"lower_bound\": 30000,\n    \"upper_bound\": 60000,\n    \"allow_instant_fill\": true,\n    \"operational_depth\": 100,\n    \"mirror_order_delay\": 0,\n    \"ignore_exchange_fees\": false,\n    \"use_existing_orders_only\": false\n    },\n  {\n    \"pair\": \"ADA/ETH\",\n    \"mode\": \"mountain\",\n    \"spread_percent\": 6,\n    \"increment_percent\": 3,\n    \"lower_bound\": 0.0003,\n    \"upper_bound\": 0.0007,\n    \"allow_instant_fill\": true,\n    \"operational_depth\": 50,\n    \"mirror_order_delay\": 0,\n    \"ignore_exchange_fees\": false,\n    \"use_existing_orders_only\": false\n  },\n  {\n    \"pair\": \"ETH/USDT\",\n    \"mode\": \"mountain\",\n    \"spread_percent\": 0.7,\n    \"increment_percent\": 0.3,\n    \"lower_bound\": 400,\n    \"upper_bound\": 500,\n    \"allow_instant_fill\": true,\n    \"operational_depth\": 50,\n    \"mirror_order_delay\": 0,\n    \"ignore_exchange_fees\": false,\n    \"use_existing_orders_only\": false\n  }\n  ]\n}"
  },
  {
    "path": "profiles/staggered_orders_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"StaggeredOrdersTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/tradingview_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            },\n            \"Ethereum\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"ETH/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 2,\n        \"complexity\": 3,\n        \"description\": \"TradingViewSignalsTrading is a profile configured to react on signals from tradingview.com. It requires to setup a trading view pro account and a webhook service. See how to configure your OctoBot for TradingView on https://www.octobot.cloud/guides/octobot-interfaces/tradingview\",\n        \"id\": \"tradingview_trading\",\n        \"name\": \"TradingView Signals Trading\",\n        \"read_only\": true\n    }\n}\n"
  },
  {
    "path": "profiles/tradingview_trading/specific_config/TradingViewSignalsTradingMode.json",
    "content": "{\n    \"close_to_current_price_difference\": 0.02,\n    \"required_strategies\": [],\n    \"use_market_orders\": true,\n    \"use_maximum_size_orders\": true\n}"
  },
  {
    "path": "profiles/tradingview_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Services\": {\n            \"TradingViewService\": true,\n            \"TradingViewServiceFeed\": true\n        },\n        \"Trading\": {\n            \"TradingViewSignalsTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "profiles/trailing_grid_trading/profile.json",
    "content": "{\n    \"config\": {\n        \"crypto-currencies\": {\n            \"Bitcoin\": {\n                \"enabled\": true,\n                \"pairs\": [\n                    \"BTC/USDT\"\n                ]\n            }\n        },\n        \"exchanges\": {\n            \"binance\": {\n                \"enabled\": true\n            }\n        },\n        \"trader\": {\n            \"enabled\": false,\n            \"load-trade-history\": false\n        },\n        \"trader-simulator\": {\n            \"enabled\": true,\n            \"fees\": {\n                \"maker\": 0.1,\n                \"taker\": 0.1\n            },\n            \"starting-portfolio\": {\n                \"BTC\": 10,\n                \"USDT\": 1000\n            }\n        },\n        \"trading\": {\n            \"reference-market\": \"USDT\",\n            \"risk\": 0.5\n        }\n    },\n    \"profile\": {\n        \"avatar\": \"default_profile.png\",\n        \"risk\": 1,\n        \"complexity\": 1,\n        \"description\": \"Trailing Grid Trading is a profile similar to the Grid Trading profile, except that it will create a trailing grid. When the BTC price will rise beyond the initial grid sell orders, the grid will automatically adapt to trade according to the updated price.\",\n        \"id\": \"trailing_grid_trading\",\n        \"name\": \"Trailing Grid Trading\",\n        \"read_only\": true\n    }\n}"
  },
  {
    "path": "profiles/trailing_grid_trading/specific_config/GridTradingMode.json",
    "content": "{\n  \"required_strategies\": [],\n  \"pair_settings\": [\n    {\n      \"pair\": \"BTC/USDT\",\n      \"flat_spread\": 1300,\n      \"flat_increment\": 800,\n      \"buy_orders_count\": 15,\n      \"sell_orders_count\": 15,\n      \"sell_funds\": 0,\n      \"buy_funds\": 0,\n      \"starting_price\": 0,\n      \"buy_volume_per_order\": 0,\n      \"sell_volume_per_order\": 0,\n      \"ignore_exchange_fees\": false,\n      \"use_fixed_volume_for_mirror_orders\": false,\n      \"mirror_order_delay\": 0,\n      \"use_existing_orders_only\": false,\n      \"allow_funds_redispatch\": false,\n      \"enable_trailing_up\": true,\n      \"enable_trailing_down\": false,\n      \"funds_redispatch_interval\": 24\n    }\n  ]\n}"
  },
  {
    "path": "profiles/trailing_grid_trading/tentacles_config.json",
    "content": "{\n    \"tentacle_activation\": {\n        \"Trading\": {\n            \"GridTradingMode\": true\n        }\n    }\n}"
  },
  {
    "path": "scripts/clear_cloudflare_cache.py",
    "content": "import os\nimport sys\nimport requests\n\n\nCLOUDFLARE_ZONE = os.getenv(\"CLOUDFLARE_ZONE\")\nCLOUDFLARE_TOKEN = os.getenv(\"CLOUDFLARE_TOKEN\")\nS3_BUCKET_NAME = os.getenv(\"S3_BUCKET_NAME\")\n\n\ndef _send_purge_cache_request(url: str, cloudflare_token: str, urls_to_purge: list):\n    with requests.post(\n        url=url,\n        headers={\n            \"Content-Type\": \"application/json\",\n            \"Authorization\": f\"Bearer {cloudflare_token}\",\n        },\n        json={\n            \"files\": urls_to_purge\n        }\n    ) as resp:\n        if resp.status_code == 200:\n            print(f\"Cache purged for {', '.join(urls_to_purge)}\")\n        else:\n            print(f\"Error when purging cache, status: {resp.status_code}, body: {resp.text}\")\n\n\ndef _get_tentacles_url(tentacle_url_identifier):\n    if not S3_BUCKET_NAME:\n        raise RuntimeError(\"Missing S3_BUCKET_NAME env variable\")\n    return os.getenv(\n        \"TENTACLES_URL\",\n        f\"https://{S3_BUCKET_NAME}.\"\n        f\"{os.getenv('TENTACLES_OCTOBOT_ONLINE_URL', 'octobot.online')}/\"\n        f\"officials/packages/full/base/\"\n        f\"{tentacle_url_identifier}/\"\n        f\"any_platform.zip\"\n    )\n\n\ndef clear_cache(tentacle_url_identifiers):\n    if not CLOUDFLARE_ZONE:\n        raise RuntimeError(\"Missing CLOUDFLARE_ZONE env variable\")\n    if not CLOUDFLARE_TOKEN:\n        raise RuntimeError(\"Missing CLOUDFLARE_TOKEN env variable\")\n    # https://developers.cloudflare.com/api/operations/zone-purge#purge-cached-content-by-url\n    url = f\"https://api.cloudflare.com/client/v4/zones/{CLOUDFLARE_ZONE}/purge_cache\"\n    _send_purge_cache_request(\n        url,\n        CLOUDFLARE_TOKEN,\n        [\n            _get_tentacles_url(tentacle_url_identifier)\n            for tentacle_url_identifier in tentacle_url_identifiers\n        ]\n    )\n\n\nif __name__ == '__main__':\n    clear_cache(sys.argv[1:])\n"
  }
]