[
  {
    "path": ".dockerignore",
    "content": ".env\n.idea\n.gitignore\n\nvenv\n\nconfig.dev.yml\ndocker-compose.yml\nReadme.md\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "# This is a basic workflow to help you get started with Actions\n\nname: Publish Docker image\n\n# Controls when the workflow will run\non:\n  # Triggers the workflow on push or pull request events but only for the master branch\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  push_to_registry:\n    name: Push Docker image to Docker Hub\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v2\n      \n      - name: Log in to Docker Hub\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n      \n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ github.actor }}/binance-pump-alerts\n      \n      - name: Build and push Docker image\n        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".gitignore",
    "content": "\nconfig.dev.yml\n\n# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,intellij+all,visualstudiocode\n# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,intellij+all,visualstudiocode\n\n### Intellij+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij+all Patch ###\n# Ignores the whole .idea folder and all .iml files\n# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360\n\n.idea/\n\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# Sonarlint plugin\n.idea/sonarlint\n\n### PyCharm+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n\n# AWS User-specific\n\n# Generated files\n\n# Sensitive or high-churn files\n\n# Gradle\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\n\n# Mongo Explorer plugin\n\n# File-based project format\n\n# IntelliJ\n\n# mpeltonen/sbt-idea plugin\n\n# JIRA plugin\n\n# Cursive Clojure plugin\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\n\n# Editor-based Rest Client\n\n# Android studio 3.1+ serialized cache file\n\n### PyCharm+all Patch ###\n# Ignores the whole .idea folder and all .iml files\n# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360\n\n\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n\n# Sonarlint plugin\n\n### Python ###\n# 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/\nshare/python-wheels/\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.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\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.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n### VisualStudioCode Patch ###\n# Ignore all local history of files\n.history\n.ionide\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3.9-slim-buster as base\n\n# Setup env\nENV LANG C.UTF-8\nENV LC_ALL C.UTF-8\nENV PYTHONDONTWRITEBYTECODE 1\nENV PYTHONFAULTHANDLER 1\nENV PATH=/home/bpauser/.local/bin:$PATH\nENV FT_APP_ENV=\"docker\"\n\n# Prepare environment\nRUN mkdir /binance-pump-alerts \\\n  && apt-get update \\\n  && apt-get -y install sudo \\\n  && apt-get clean \\\n  && useradd -u 1000 -G sudo -U -m bpauser \\\n  && chown bpauser:bpauser /binance-pump-alerts \\\n  # Allow sudoers\n  && echo \"bpauser ALL=(ALL) NOPASSWD: /bin/chown\" >> /etc/sudoers\n\nWORKDIR /binance-pump-alerts\n\n# Install dependencies\nFROM base as python-deps\nRUN  apt-get update \\\n  && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \\\n  && apt-get clean \\\n  && pip install --upgrade pip\n\n# Install dependencies\nCOPY --chown=bpauser:bpauser requirements.txt /binance-pump-alerts/\nUSER bpauser\nRUN  pip install --user --no-cache-dir -r requirements.txt\n\n# Copy dependencies to runtime-image\nFROM base as runtime-image\n\nCOPY --from=python-deps /usr/local/lib /usr/local/lib\nENV LD_LIBRARY_PATH /usr/local/lib\n\nCOPY --from=python-deps --chown=bpauser:bpauser /home/bpauser/.local /home/bpauser/.local\n\nUSER bpauser\n\nCOPY --chown=bpauser:bpauser . /binance-pump-alerts/\n\nRUN chmod a+x entrypoint.sh\n\nENTRYPOINT [\"./entrypoint.sh\", \"python\", \"pumpAlerts.py\"]\n"
  },
  {
    "path": "README.md",
    "content": "# Binance Pump Alerts\n\nBPA is a simple application which gets the price data from Binance Spot or Futures API and sends Telegram messages based on parameters set used to detect pumps and dumps on the Binance Exchange.\n\n[Demo Telegram Channel](https://t.me/binance_pump_alerts) hosted on AWS ec2 running the 'Base Stable Version' release 24/7.\n\n![image](https://user-images.githubusercontent.com/63389110/128601355-4be90b36-5e54-4be6-bf85-00fc395645de.png)\n\n## Manual Setup\n\n1. On the command-line, run the command `pip install -r requirements.txt` while located at folder with code.\n1. Create a new telegram bot token from [@botfather](https://t.me/BotFather).\n1. Get telegram `chat_id` from [@get_id_bot](https://telegram.me/get_id_bot).\n   - Alternatively, a `channel_id` can be used as well.\n1. Add pairs to watch into the watchlist or to ignore in blacklist or leave it empty to monitor all tickers on Binance.\n1. Run the script with the command `python pumpAlerts.py`.\n\n## Docker Setup\n\n1. Use environment variables in the `docker-compse.yml` file to provide your config.\n   - See `entrypoint.sh` for environment variable names and the config possibilities.\n   - You can also use a `.env` file during development.\n   - If changing the config parameters, you have to make sure that search and replace will place the right parameter in the `config.yml`\n   - Emojis are more tricky therefore defining it with some tricks e.g. `PUMP_EMOJI=\"! \\\"\\\\\\\\U0001F4B9\\\"\"`\n1. On the command line run `docker-compose up -d --build` to create and run the docker image/container.\n\n## Configuration\n\n### Mandatory Params\n\n1. `telegramToken`: The token obtained from[@botfather](https://t.me/BotFather).\n2. `telegramChatId`: The bot will send the messages to this `chat_id`. It can be a group or channel as well.\n\n## Main Customizable Params\n\n1. `chartIntervals`: Can be modified to consider other timeframes, follow the format of 's' for seconds, 'm' for minutes, 'h' for hours.\n1. `outlierIntervals`: (0.01 -> 1% , 0.1 -> 10%), modify accordingly based on needs. Avoid setting it too low to avoid noise.\n1. `extractInterval`: Default is `1s`, Interval at which we retrieve the price information from Binance.\n1. `pairsOfInterest`: Default is _USDT_. Other options include BUSD, BTC, ETH etc.\n1. `topReportIntervals`: Default is `1h`,`3h`and `6h` Intervals for top pump and dump reports to be sent, ensure it is in chartIntervals + outlierIntervals as well.\n\n### Optional features to enable\n\n1. `watchlist`: Default if left empty it'll look at ALL symbols after filtering by pairs of interest. If pairs are added to the watchlist, the application will _only TRACK the pairs specified_. pairsOfInterest will be ignored.\n1. `blacklist`: Default if left empty it'll look at ALL symbols after filtering by pairs of interest. If pairs are added to the blacklist, the application will ignore pairs specified. pairs of Interest will NOT be impacted.\n1. `dumpEnabled`: If `True`, the application will alert on dumps as well.\n\n#### Top Pump & Dump Params\n\n1. `topPumpEnabled`: If `True`, the application will send the Top X pumps at the defined interval.\n1. `topDumpEnabled`: If `True`, the application will send the Top X dumps at the defined interval.\n   - Together with pump information, if enabled.\n1. `noOfReportedCoins`: Top X amount of coins shown, adjust to show more or less within the timeframe.\n1. `telegramAlertChatId`: Insert the alert chat_id for top pump dump alert, if left at `0`, it'll send messages to the telegram `chat_Id`.\n   For params not indicated above, refer to comments besides parameter for its use.\n\n#### Debug Params (Avoid modifying if possible!)\n\n1. `debug`: Default is `False`. Please, only enable for debugging purposes. Default logging set to info level.\n1. `resetInterval`: Default `12h`. It clears the array used to store data price points to prevent memory issues.\n1. `priceRetryInterval`: Default `5s`. In the case of get price fail, this is the time delay before re-attempt\n1. `checkNewListingEnabled`: Default `True`. Enables checking and adding of new listing pairs.\n\n## Todo\n\n1. Integrate with Binance API to make trades on pumps.\n1. Integrate with Binance Websocket API to get volume information.\n1. Integrate with listing-predictor to monitor movements for potential listings.\n\n## Completed features\n\n1. Telegram integration\n1. Price update every 1s\n1. Adjustable alert % param (outliers)\n1. Watchlist feature\n1. Monitor future markets\n1. Optional alert on dumps\n1. Customizable minimum alert interval for spam prevention\n1. Option to disable print debugs on extraction\n1. [Test] Volume Change Updates (TEST_VOL version)\n1. Allows long period of running without memory issues\n1. Send periodic Top X Pump & Dump reports\n1. Docker integration (Thanks to [@patbaumgartner](https://github.com/patbaumgartner))\n1. Logging integration (Thanks to [@patbaumgartner](https://github.com/patbaumgartner))\n1. Major Refactoring and cleanup (Thanks to [@patbaumgartner](https://github.com/patbaumgartner))\n1. Blacklist feature\n"
  },
  {
    "path": "alerter/BinancePumpAndDumpAlerter.py",
    "content": "import logging\r\nimport requests\r\nimport time\r\n\r\nfrom time import sleep\r\nfrom utils import ConversionUtils\r\n\r\n\r\nclass BinancePumpAndDumpAlerter:\r\n    def __init__(\r\n        self,\r\n        api_url,\r\n        watchlist,\r\n        blacklist,\r\n        pairs_of_interest,\r\n        chart_intervals,\r\n        outlier_intervals,\r\n        top_report_intervals,\r\n        extract_interval,\r\n        retry_interval,\r\n        reset_interval,\r\n        top_pump_enabled,\r\n        top_dump_enabled,\r\n        additional_statistics_enabled,\r\n        no_of_reported_coins,\r\n        dump_enabled,\r\n        check_new_listing_enabled,\r\n        top_report_nearest_hour,\r\n        telegram,\r\n        report_generator,\r\n    ):\r\n        self.api_url = api_url\r\n        self.watchlist = watchlist\r\n        self.blacklist = blacklist\r\n        self.pairs_of_interest = pairs_of_interest\r\n        self.outlier_intervals = outlier_intervals\r\n        self.extract_interval = extract_interval\r\n        self.retry_interval = retry_interval\r\n        self.reset_interval = reset_interval\r\n        self.top_pump_enabled = top_pump_enabled\r\n        self.top_dump_enabled = top_dump_enabled\r\n        self.additional_statistics_enabled = additional_statistics_enabled\r\n        self.no_of_reported_coins = no_of_reported_coins\r\n        self.dump_enabled = dump_enabled\r\n        self.check_new_listing_enabled = check_new_listing_enabled\r\n        self.telegram = telegram\r\n        self.report_generator = report_generator\r\n\r\n        self.logger = logging.getLogger(\"pump-and-dump-alerter\")\r\n\r\n        self.initial_time = int(time.time())\r\n        nearest_hour = self.initial_time - (self.initial_time % 3600) + 3600\r\n        self.logger.info(\r\n            \"Nearest hour is %i seconds away\", nearest_hour - self.initial_time\r\n        )\r\n\r\n        self.chart_intervals = {}\r\n        for interval in chart_intervals:\r\n            self.chart_intervals[interval] = {}\r\n            self.chart_intervals[interval][\r\n                \"value\"\r\n            ] = ConversionUtils.duration_to_seconds(interval)\r\n\r\n        self.top_report_intervals = {}\r\n        for interval in top_report_intervals:\r\n            self.top_report_intervals[interval] = {}\r\n\r\n            # Determine initial start time for TPD. Should conveniently solve original 0% issue together.\r\n            if top_report_nearest_hour:\r\n                self.top_report_intervals[interval][\"start\"] = nearest_hour\r\n            else:\r\n                self.top_report_intervals[interval][\"start\"] = self.initial_time\r\n\r\n            self.top_report_intervals[interval][\r\n                \"value\"\r\n            ] = ConversionUtils.duration_to_seconds(interval)\r\n\r\n    @staticmethod\r\n    def extract_ticker_data(symbol, assets):\r\n        for asset in assets:\r\n            if asset[\"symbol\"] == symbol:\r\n                return asset\r\n\r\n    @staticmethod\r\n    def create_new_asset(symbol, chart_intervals):\r\n        asset = {\"symbol\": symbol, \"price\": [], \"volume\": []}\r\n\r\n        for interval in chart_intervals:\r\n            asset[interval] = {}\r\n            asset[interval][\"change_current\"] = 0\r\n            asset[interval][\"change_last\"] = 0\r\n            asset[interval][\"change_volume\"] = 0\r\n\r\n        return asset\r\n\r\n    def retrieve_exchange_assets(self, api_url):\r\n        try:\r\n            self.logger.debug(\r\n                \"Retrieving price information from the ticker. ApiUrl: %s.\", api_url\r\n            )\r\n            return requests.get(api_url).json()\r\n        except Exception as e:\r\n            self.logger.error(\r\n                \"Issue occurred while getting prices. Error: %s.\",\r\n                e,\r\n                exc_info=True,\r\n            )\r\n            sleep(5)  # Sleep 5s and try again\r\n            return self.retrieve_exchange_assets(api_url)\r\n\r\n    def is_symbol_valid(self, symbol, watchlist, blacklist, pairs_of_interest):\r\n        # Filter symbols in watchlist if set - This disables the pairsOfInterest feature\r\n        if len(watchlist) > 0:\r\n            if symbol not in watchlist:\r\n                self.logger.debug(\"Ignoring symbol not in watchlist: %s.\", symbol)\r\n                return False\r\n            return True\r\n\r\n        # Filter symbols in blacklist if set - This DOES NOT IMPACT the pairsOfInterest feature\r\n        if len(blacklist) > 0:\r\n            if symbol in blacklist:\r\n                self.logger.info(\r\n                    \"Ignoring symbol found in blacklist: %s.\", symbol)\r\n                return False\r\n\r\n        # Filter pairsOfInterest to reduce the noise. E.g. BUSD, USDT, ETH, BTC\r\n        is_in_pairs_of_interest = False\r\n        for pair in pairs_of_interest:\r\n            if symbol.endswith(pair):\r\n                is_in_pairs_of_interest = True\r\n                break\r\n\r\n        if not is_in_pairs_of_interest:\r\n            self.logger.debug(\"Ignoring symbol not in pairsOfInterests: %s.\", symbol)\r\n            return False\r\n\r\n        # Filter leverage symbols\r\n        for pair in pairs_of_interest:\r\n            coin = symbol.replace(pair, \"\")\r\n            if (\r\n                coin.endswith(\"UP\")\r\n                or coin.endswith(\"DOWN\")\r\n                or coin.endswith(\"BULL\")\r\n                or coin.endswith(\"BEAR\")\r\n            ):\r\n                self.logger.debug(\"Ignoring leverage symbol: %s.\", symbol)\r\n                return False\r\n\r\n        return True\r\n\r\n    def filter_and_convert_assets(\r\n        self, exchange_assets, watchlist, blacklist, pairs_of_interest, chart_intervals\r\n    ):\r\n        filtered_assets = []\r\n\r\n        for exchange_asset in exchange_assets:\r\n            symbol = exchange_asset[\"symbol\"]\r\n\r\n            if self.is_symbol_valid(symbol, watchlist, blacklist, pairs_of_interest):\r\n                filtered_assets.append(self.create_new_asset(symbol, chart_intervals))\r\n                self.logger.info(\"Adding symbol: %s.\", symbol)\r\n\r\n        return filtered_assets\r\n\r\n    def update_all_monitored_assets_and_send_news_messages(\r\n        self,\r\n        monitored_assets,\r\n        exchange_assets,\r\n        current_time,\r\n        dump_enabled,\r\n        chart_intervals,\r\n        extract_interval,\r\n        outlier_intervals,\r\n    ):\r\n        for asset in monitored_assets:\r\n            exchange_asset = self.extract_ticker_data(asset[\"symbol\"], exchange_assets)\r\n            asset[\"price\"].append(float(exchange_asset[\"price\"]))\r\n\r\n            self.calculate_asset_change(\r\n                asset,\r\n                chart_intervals,\r\n                extract_interval,\r\n            )\r\n\r\n            self.report_generator.send_pump_dump_message(\r\n                asset,\r\n                chart_intervals,\r\n                outlier_intervals,\r\n                current_time,\r\n                dump_enabled,\r\n            )\r\n\r\n    def calculate_asset_change(\r\n        self,\r\n        asset,\r\n        chart_intervals,\r\n        extract_interval,\r\n    ):\r\n        asset_length = len(asset[\"price\"])\r\n\r\n        for interval in chart_intervals:\r\n            data_points = chart_intervals[interval][\"value\"] // extract_interval\r\n\r\n            # If data is not enough yet after restart for interval, stop here.\r\n            if data_points >= asset_length:\r\n                self.logger.debug(\r\n                    \"Not enough datapoints (%s/%s) for interval: %s\",\r\n                    asset_length,\r\n                    data_points,\r\n                    interval,\r\n                )\r\n                break\r\n\r\n            # Gets change in % from last alert trigger.\r\n            current_price = asset[\"price\"][-1]\r\n            if current_price == 0:\r\n                self.logger.warning(\r\n                    \"Received zero price for asset %s, skipping calculation\",\r\n                    asset[\"symbol\"]\r\n                )\r\n                change = 0\r\n            else:\r\n                price_delta = current_price - asset[\"price\"][-1 - data_points]\r\n                change = price_delta / current_price\r\n\r\n            self.logger.debug(\r\n                \"Calculated asset: %s for interval: %s with change: %s\",\r\n                asset[\"symbol\"],\r\n                interval,\r\n                change,\r\n            )\r\n\r\n            # Set last change for next interval iteration\r\n            asset[interval][\"change_last\"] = asset[interval][\"change_current\"]\r\n\r\n            # Stores change for the current interval into asset dict.\r\n            asset[interval][\"change_current\"] = change\r\n\r\n        return asset\r\n\r\n    def reset_prices_data_when_due(\r\n        self,\r\n        initial_time,\r\n        current_time,\r\n        reset_interval,\r\n        extract_interval,\r\n        assets,\r\n        chart_intervals,\r\n    ):\r\n        if current_time - initial_time > reset_interval:\r\n\r\n            message = \"Emptying price data to prevent memory errors.\"\r\n            self.logger.debug(message)\r\n            self.telegram.send_generic_message(message, is_alert_chat=True)\r\n\r\n            # Do not delete everything, only elements older than the last monitored interval\r\n            lastInterval = \"1s\"\r\n            for interval in chart_intervals:\r\n                lastInterval = interval\r\n\r\n            data_points = chart_intervals[lastInterval][\"value\"] // extract_interval\r\n\r\n            for asset in assets:\r\n                asset[\"price\"] = asset[\"price\"][-1 - data_points :]\r\n\r\n            initial_time = current_time\r\n\r\n        return initial_time\r\n\r\n    def add_new_asset_listings(\r\n        self,\r\n        initial_assets,\r\n        filtered_assets,\r\n        exchange_assets,\r\n        watchlist,\r\n        blacklist,\r\n        pairs_of_interest,\r\n        chart_intervals,\r\n    ):\r\n\r\n        if len(initial_assets) >= len(exchange_assets):\r\n            # If initial_assets has more than assets we just ignore it\r\n            self.logger.debug(\"No new listing found.\")\r\n            return filtered_assets\r\n\r\n        init_symbols = [asset[\"symbol\"] for asset in initial_assets]\r\n        retrieved_symbols_to_add = [\r\n            exchange_asset[\"symbol\"]\r\n            for exchange_asset in exchange_assets\r\n            if exchange_asset[\"symbol\"] not in init_symbols\r\n        ]\r\n\r\n        self.logger.debug(\"New listings found: %s.\", retrieved_symbols_to_add)\r\n\r\n        filtered_symbols_to_add = []\r\n        for symbol in retrieved_symbols_to_add:\r\n            if self.is_symbol_valid(symbol, watchlist, blacklist, pairs_of_interest):\r\n                filtered_symbols_to_add.append(symbol)\r\n                filtered_assets.append(self.create_new_asset(symbol, chart_intervals))\r\n\r\n        self.logger.debug(\"Filtered new listings found: %s.\", filtered_symbols_to_add)\r\n\r\n        if len(filtered_symbols_to_add) > 0:\r\n            self.report_generator.send_new_listings(filtered_symbols_to_add)\r\n\r\n        return filtered_assets\r\n\r\n    def check_and_send_top_pump_dump_statistics_report(\r\n        self,\r\n        assets,\r\n        current_time,\r\n        top_report_intervals,\r\n        top_pump_enabled,\r\n        top_dump_enabled,\r\n        additional_stats_enabled,\r\n        no_of_reported_coins,\r\n    ):\r\n\r\n        for interval in top_report_intervals:\r\n\r\n            if (\r\n                current_time\r\n                > top_report_intervals[interval][\"start\"]\r\n                + top_report_intervals[interval][\"value\"]\r\n                + 1\r\n            ):\r\n            \r\n                # Update time for new trigger, rounded down to nearest interval. Avoid delay over time.\r\n                top_report_intervals[interval][\"start\"] = current_time - (current_time % ConversionUtils.duration_to_seconds(interval))\r\n\r\n                self.logger.debug(\r\n                    \"Sending out top pump dump report. Interval: %s.\", interval\r\n                )\r\n\r\n                self.report_generator.send_top_pump_dump_statistics_report(\r\n                    assets,\r\n                    interval,\r\n                    top_pump_enabled,\r\n                    top_dump_enabled,\r\n                    additional_stats_enabled,\r\n                    no_of_reported_coins,\r\n                )\r\n\r\n    def run(self):\r\n\r\n        initial_assets = self.retrieve_exchange_assets(self.api_url)\r\n\r\n        filtered_assets = self.filter_and_convert_assets(\r\n            initial_assets,\r\n            self.watchlist,\r\n            self.blacklist,\r\n            self.pairs_of_interest,\r\n            self.chart_intervals,\r\n        )\r\n\r\n        message = \"*Bot has started.* Following _{0}_ pairs.\"\r\n        self.telegram.send_generic_message(message, len(filtered_assets))\r\n        if self.telegram.is_alert_chat_enabled():\r\n            self.telegram.send_generic_message(\r\n                message,\r\n                len(filtered_assets),\r\n                is_alert_chat=True,\r\n            )\r\n\r\n        while True:\r\n            start_loop_time = time.time()\r\n            loop_time = int(start_loop_time)\r\n\r\n            exchange_assets = self.retrieve_exchange_assets(self.api_url)\r\n\r\n            if self.check_new_listing_enabled:\r\n                filtered_assets = self.add_new_asset_listings(\r\n                    initial_assets,\r\n                    filtered_assets,\r\n                    exchange_assets,\r\n                    self.watchlist,\r\n                    self.blacklist,\r\n                    self.pairs_of_interest,\r\n                    self.chart_intervals,\r\n                )\r\n                # Reset initial exchange asset\r\n                initial_assets = exchange_assets\r\n\r\n            self.update_all_monitored_assets_and_send_news_messages(\r\n                filtered_assets,\r\n                exchange_assets,\r\n                loop_time,\r\n                self.dump_enabled,\r\n                self.chart_intervals,\r\n                self.extract_interval,\r\n                self.outlier_intervals,\r\n            )\r\n\r\n            self.check_and_send_top_pump_dump_statistics_report(\r\n                filtered_assets,\r\n                loop_time,\r\n                self.top_report_intervals,\r\n                self.top_pump_enabled,\r\n                self.top_dump_enabled,\r\n                self.additional_statistics_enabled,\r\n                self.no_of_reported_coins,\r\n            )\r\n\r\n            self.initial_time = self.reset_prices_data_when_due(\r\n                self.initial_time,\r\n                loop_time,\r\n                self.reset_interval,\r\n                self.extract_interval,\r\n                filtered_assets,\r\n                self.chart_intervals,\r\n            )\r\n\r\n            # Sleeps for the remainder of 1s, or loops through if extraction takes longer\r\n            end_loop_time = time.time()\r\n\r\n            self.logger.info(\r\n                \"Extracting loop started at %d and finished at %d. Taking %f seconds.\",\r\n                start_loop_time,\r\n                end_loop_time,\r\n                end_loop_time - start_loop_time,\r\n            )\r\n\r\n            if end_loop_time < start_loop_time + self.extract_interval:\r\n                sleep_time = start_loop_time + self.extract_interval - end_loop_time\r\n                self.logger.debug(\"Now sleeping %f seconds.\", sleep_time)\r\n                sleep(sleep_time)\r\n"
  },
  {
    "path": "alerter/__init__.py",
    "content": "from .BinancePumpAndDumpAlerter import BinancePumpAndDumpAlerter\n"
  },
  {
    "path": "config.yml",
    "content": "# Binance Pump and Dump Alert Configuration\n\n# Using Spot API with url set to https://api.binance.com/api/v3/ticker/price\n# Using Futures API with url set to: https://fapi.binance.com/fapi/v1/ticker/price\napiUrl: https://api.binance.com/api/v3/ticker/price\n\n# Intervals which are monitored. Add or remove intervals as you like.\nchartIntervals:\n  - 1s\n  - 5s\n  - 15s\n  - 30s\n  - 1m\n  - 5m\n  - 15m\n  - 30m\n  - 1h\n  - 3h\n  - 6h\n\n# Values in % at which an alert is triggered. Ensure interval exists in chartIntervals as well.\noutlierIntervals:\n  \"1s\": 0.02\n  \"5s\": 0.05\n  \"15s\": 0.06\n  \"30s\": 0.08\n  \"1m\": 0.1\n  \"5m\": 0.10\n  \"15m\": 0.15\n  \"30m\": 0.20\n  \"1h\": 0.30\n  \"3h\": 0.4\n  \"6h\": 0.5\n\n# Used for telegram bot updates\n\n# Insert telegramToken obtained from @BotFather here\ntelegramToken: mySecretToken\n# Insert Chat ID obtained from @get_id_bot here\ntelegramChatId: mySecretChatId\n# Insert Chat ID for top pump dump alert, if left on `0` it'll send the message to telegram chat id\ntelegramAlertChatId: 0\n\n# Useful Params\n\n# Interval between each price extract from Binance REST API\nextractInterval: 1s\n\n# Watchlist only mode, if enabled, ONLY pairs in watchlist will be monitored\n# E.g. 'ADAUSDT', 'ETHUSDT'\n# watchlist:\n#  - ADAUSDT\n#  - ETHUSDT\n\n# Blacklist only mode, if enabled, pairs in blacklist will be IGNORED it DOES NOT IMPACT pairsOfInterest\nblacklist:\n  - NBTUSDT\n#  - ETHUSDT\n\n# List the trading currency you are interested in. Other options include 'BUSD', 'BTC' , 'ETH'\npairsOfInterest:\n  - USDT\n\n# Feature params\n\n# Determine whether to look at DUMPs\ndumpEnabled: True\n\n# Top Pump & Dump Feature Params\n\n# Set to false if not interested in top pump info\ntopPumpEnabled: True\n# Set to false if not interested in top dump info\ntopDumpEnabled: True\n# Set to false if not interested in net movement of coins\nadditionalStatsEnabled: True\n# Top X amount of coins shown in the interval report, adjust to show more or less within the timeframe\nnoOfReportedCoins: 5\n# Intervals for top pump and dump to be sent, ensure its in chartIntervals + outlierIntervals as well\ntopReportIntervals:\n  - 3h\n  - 6h\n\n# Define your own bot emojis.\nbotEmoji: ! \"\\U0001F916\" # 🤖\npumpEmoji: ! \"\\U0001F7E2\" # 🟢 or '\\U0001F4C8' 📈 '\\U0001F53C'🔼\ndumpEmoji: ! \"\\U0001F534\" # 🔴 or '\\U0001F4C9' 📉 '\\U0001F53D'🔽\ntopEmoji: ! \"\\U0001F3C6\" # 🏆\nnewsEmoji: ! \"\\U0001F4F0\" # 📰 or '\\U0001F680' 🚀\n\n# Debug Params (Avoid touching it if there's no issues)\n\n# If False we do not print unnecessary messages\ndebug: False\n# Skip alert at higher timeframes when change in % did not change value by threshold in percentage points\nalertSkipThreshold: 0.75\n# Interval for clearing array to prevent memory can handle up to 12h+ depending on system\nresetInterval: 6h\n# In the case of get price fail, this is the time delay before re-attempt\npriceRetryInterval: 5s\n# Disables checking and adding of new listing pairs\ncheckNewListingEnabled: True\n# Disables rounding to nearest hour for first TPD if false\ntopReportNearestHour: True\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3.8\"\n\nservices:\n  binance-pump-alerts:\n    build:\n      context: .\n      dockerfile: \"./Dockerfile\"\n    image: patbaumgartner/binance-pump-alerts:latest\n    # image: brianleect/binance-pump-alerts:latest\n    restart: \"no\"\n    environment:\n      - TELEGRAM_TOKEN=${TELEGRAM_TOKEN}\n      - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}\n      - TELEGRAM_ALERT_CHAT_ID=${TELEGRAM_ALERT_CHAT_ID}\n      - PUMP_EMOJI=${PUMP_EMOJI}\n      - DUMP_EMOJI=${DUMP_EMOJI}\n      - NO_OF_REPORTED_COINS=${NO_OF_REPORTED_COINS}\n      - DEBUG=${DEBUG}\n      - RESET_INTERVAL=${RESET_INTERVAL}\n"
  },
  {
    "path": "entrypoint.sh",
    "content": "#!/bin/bash\nset -e\n\nprocess_config() {\n\n    # Please mount your own config file into the container for unsupported env parameters!\n    # Currently not supported config parameters: chartIntervals, outlierIntervals, pairsOfInterest, watchlist, blacklist, topReportIntervals\n\n    if [[ -n $API_URL ]]; then\n        sed -i \"s/apiUrl.*/apiUrl: ${API_URL}/\" config.yml\n    fi\n\n    if [[ -n $TELEGRAM_TOKEN ]]; then\n        sed -i \"s/telegramToken.*/telegramToken: ${TELEGRAM_TOKEN}/\" config.yml\n    fi\n    if [[ -n $TELEGRAM_CHAT_ID ]]; then\n        sed -i \"s/telegramChatId.*/telegramChatId: ${TELEGRAM_CHAT_ID}/\" config.yml\n    fi\n    if [[ -n $TELEGRAM_ALERT_CHAT_ID ]]; then\n        sed -i \"s/telegramAlertChatId.*/telegramAlertChatId: ${TELEGRAM_ALERT_CHAT_ID}/\" config.yml\n    fi\n\n    if [[ -n $EXTRACT_INTERVAL ]]; then\n        sed -i \"s/extractInterval.*/extractInterval: ${EXTRACT_INTERVAL}/\" config.yml\n    fi\n\n    if [[ -n $DUMP_ENABLED ]]; then\n        sed -i \"s/dumpEnabled.*/dumpEnabled: ${DUMP_ENABLED}/\" config.yml\n    fi\n    \n    if [[ -n $TOP_PUMP_ENABLED ]]; then\n        sed -i \"s/topPumpEnabled.*/topPumpEnabled: ${TOP_PUMP_ENABLED}/\" config.yml\n    fi\n    if [[ -n $TOP_DUMP_ENABLED ]]; then\n        sed -i \"s/topDumpEnabled.*/topDumpEnabled: ${TOP_DUMP_ENABLED}/\" config.yml\n    fi\n    if [[ -n $ADDITIONAL_STATS_ENABLED ]]; then\n        sed -i \"s/additionalStatsEnabled.*/additionalStatsEnabled: ${ADDITIONAL_STATS_ENABLED}/\" config.yml\n    fi\n    if [[ -n $NO_OF_REPORTED_COINS ]]; then\n        sed -i \"s/noOfReportedCoins.*/noOfReportedCoins: ${NO_OF_REPORTED_COINS}/\" config.yml\n    fi\n\n    if [[ -n $BOT_EMOJI ]]; then\n        sed -i \"s/botEmoji.*/botEmoji: ${BOT_EMOJI}/\" config.yml\n    fi\n    if [[ -n $PUMP_EMOJI ]]; then\n        sed -i \"s/pumpEmoji.*/pumpEmoji: ${PUMP_EMOJI}/\" config.yml\n    fi\n    if [[ -n $DUMP_EMOJI ]]; then\n        sed -i \"s/dumpEmoji.*/dumpEmoji: ${DUMP_EMOJI}/\" config.yml\n    fi\n    if [[ -n $TOP_EMOJI ]]; then\n        sed -i \"s/topEmoji.*/topEmoji: ${TOP_EMOJI}/\" config.yml\n    fi\n    if [[ -n $NEWS_EMOJI ]]; then\n        sed -i \"s/newsEmoji.*/newsEmoji: ${NEWS_EMOJI}/\" config.yml\n    fi\n\n    if [[ -n $DEBUG ]]; then\n        sed -i \"s/debug.*/debug: ${DEBUG}/\" config.yml\n    fi\n    if [[ -n $ALERT_SKIP_THRESHOLD ]]; then\n        sed -i \"s/alertSkipThreshold.*/alertSkipThreshold: ${ALERT_SKIP_THRESHOLD}/\" config.yml\n    fi\n    if [[ -n $RESET_INTERVAL ]]; then\n        sed -i \"s/resetInterval.*/resetInterval: ${RESET_INTERVAL}/\" config.yml\n    fi\n    if [[ -n $PRICE_RETRY_INTERVAL ]]; then\n        sed -i \"s/priceRetryInterval.*/priceRetryInterval: ${PRICE_RETRY_INTERVAL}/\" config.yml\n    fi\n    if [[ -n $CHECK_NEW_LISTING_ENABLED ]]; then\n        sed -i \"s/checkNewListingEnabled.*/checkNewListingEnabled: ${CHECK_NEW_LISTING_ENABLED}/\" config.yml\n    fi\n    if [[ -n $TOP_REPORT_NEAREST_HOUR ]]; then\n        sed -i \"s/topReportNearestHour.*/topReportNearestHour: ${TOP_REPORT_NEAREST_HOUR}/\" config.yml\n    fi\n}\n\n# Adding parameters set from the environment variables to the config yaml file.\nprocess_config\n\necho \necho \"Running $@\"\necho\n\nexec \"$@\"\n"
  },
  {
    "path": "pumpAlerts.py",
    "content": "import colorlog, logging\nimport os\nimport yaml\n\nfrom alerter import BinancePumpAndDumpAlerter\nfrom reporter import ReportGenerator\nfrom sender import TelegramSender\nfrom utils import ConversionUtils\n\n# Read config\n__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))\nconfig_file = \"config.yml\"\n\n# Using dev config while development\nconfig_dev_file = \"config.dev.yml\"\nif os.path.isfile(config_dev_file):\n    config_file = config_dev_file\n\nyaml_file = open(os.path.join(__location__, config_file), \"r\", encoding=\"utf-8\")\n\nconfig = yaml.load(yaml_file, Loader=yaml.FullLoader)\n\n# Define the log format\nbold_seq = \"\\033[1m\"\nlog_format = \"[%(asctime)s] %(processName)-12s %(threadName)-23s %(levelname)-8s %(name)-23s %(message)s\"\ncolor_format = f\"{bold_seq} \" \"%(log_color)s \" f\"{log_format}\"\n\ncolorlog.basicConfig(\n    # Define logging level according to the configuration\n    level=logging.DEBUG if config[\"debug\"] is True else logging.INFO,\n    # Declare the object we created to format the log messages\n    format=color_format,\n    # Declare handlers for the Console\n    handlers=[logging.StreamHandler()],\n)\n\n# Define your logger name\nlogger = logging.getLogger(\"binance-pump-alerts-app\")\n\n# Logg whole configuration during the startup\nlogger.info(\"Using config file: %s\", config_file)\nlogger.debug(\"Config: %s\", config)\n\n\ndef main():\n    telegram = TelegramSender(\n        token=config[\"telegramToken\"],\n        chat_id=config[\"telegramChatId\"],\n        alert_chat_id=config[\"telegramAlertChatId\"]\n        if \"telegramAlertChatId\" in config and config[\"telegramAlertChatId\"] != 0\n        else config[\"telegramChatId\"],\n        bot_emoji=config[\"botEmoji\"],\n        top_emoji=config[\"topEmoji\"],\n        news_emoji=config[\"newsEmoji\"],\n    )\n\n    reporter = ReportGenerator(\n        telegram=telegram,\n        alert_skip_threshold=config[\"alertSkipThreshold\"],\n        pump_emoji=config[\"pumpEmoji\"],\n        dump_emoji=config[\"dumpEmoji\"],\n    )\n\n    alerter = BinancePumpAndDumpAlerter(\n        api_url=config[\"apiUrl\"],\n        watchlist=[] if \"watchlist\" not in config else config[\"watchlist\"],\n        blacklist=[] if \"blacklist\" not in config else config[\"blacklist\"],\n        pairs_of_interest=config[\"pairsOfInterest\"],\n        chart_intervals=config[\"chartIntervals\"],\n        outlier_intervals=config[\"outlierIntervals\"],\n        top_report_intervals=config[\"topReportIntervals\"],\n        extract_interval=ConversionUtils.duration_to_seconds(config[\"extractInterval\"]),\n        retry_interval=ConversionUtils.duration_to_seconds(\n            config[\"priceRetryInterval\"]\n        ),\n        reset_interval=ConversionUtils.duration_to_seconds(config[\"resetInterval\"]),\n        top_pump_enabled=config[\"topPumpEnabled\"],\n        top_dump_enabled=config[\"topDumpEnabled\"],\n        additional_statistics_enabled=config[\"additionalStatsEnabled\"],\n        no_of_reported_coins=config[\"noOfReportedCoins\"],\n        dump_enabled=config[\"dumpEnabled\"],\n        check_new_listing_enabled=config[\"checkNewListingEnabled\"],\n        top_report_nearest_hour=config[\"topReportNearestHour\"],\n        telegram=telegram,\n        report_generator=reporter,\n    )\n\n    alerter.run()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "reporter/ReportGenerator.py",
    "content": "import logging\n\nfrom datetime import datetime\n\n\nclass ReportGenerator:\n    def __init__(\n        self,\n        telegram,\n        alert_skip_threshold,\n        pump_emoji=\"\\U0001F7E2\",  # 🟢\n        dump_emoji=\"\\U0001F534\",  # 🔴\n    ):\n        self.telegram = telegram\n        self.alert_skip_threshold = alert_skip_threshold\n        self.pump_emoji = pump_emoji\n        self.dump_emoji = dump_emoji\n\n        self.logger = logging.getLogger(\"report-generator\")\n\n    def send_pump_message(self, symbol, interval, change, price):\n        self.telegram.send_message(\n            \"\"\"\\\n{0} *{1} [{2} Interval]* | Change: _{3:.3f}%_ | Price: _{4:.10f}_\n\nOpen in [Binance Spot](https://www.binance.com/en/trade/{1})\\\n            \"\"\".format(\n                self.pump_emoji, symbol, interval, change * 100, price\n            ),\n            is_alert_chat=False,\n        )\n\n    def send_dump_message(self, symbol, interval, change, price):\n        self.telegram.send_message(\n            \"\"\"\\\n{0} *{1} [{2} Interval]* | Change: _{3:.3f}%_ | Price: _{4:.10f}_\n\nOpen in [Binance Spot](https://www.binance.com/en/trade/{1})\\\n            \"\"\".format(\n                self.dump_emoji, symbol, interval, change * 100, price\n            ),\n            is_alert_chat=False,\n        )\n\n    def send_new_listings(self, symbols_to_add):\n        message = \"\"\"\\\n*New Listings*\"\n{0} new pairs found, adding to monitored list.\"\n\n*Adding Pairs:*\\\n            \"\"\".format(\n            len(symbols_to_add)\n        )\n\n        message += \"\\n\"\n        for symbol in symbols_to_add:\n            message += \"- _{0}_\\n\".format(symbol)\n\n        self.telegram.send_news_message(message, is_alert_chat=True)\n\n    def send_pump_dump_message(\n        self,\n        asset,\n        chart_intervals,\n        outlier_intervals,\n        current_time,\n        dump_enabled=True,\n    ):\n        change_biggest_delta = 0\n        no_of_alerts = 0\n        message = \"\"\n\n        for interval in chart_intervals:\n\n            change = asset[interval][\"change_current\"]\n\n            # Skip report if no outlier\n            if abs(change) < outlier_intervals[interval]:\n                self.logger.debug(\n                    \"Change for asset: %s for interval: %s is to low: %s. Skipping report creation.\",\n                    asset[\"symbol\"],\n                    interval,\n                    change,\n                )\n                continue\n\n            # Remember biggest change of all intervals, to skip later notification\n            change_last = asset[interval][\"change_last\"]\n            change_delta = change - change_last\n\n            if abs(change_delta) > abs(change_biggest_delta):\n                change_biggest_delta = change_delta\n\n            # Remember the total number of alerts\n            no_of_alerts += 1\n\n            if change > 0:\n                message += \"{0} *{1} Interval* | Change: _{2:.3f}%_\\n\".format(\n                    self.pump_emoji,\n                    interval,\n                    change * 100,\n                    asset[\"price\"][-1],\n                )\n\n            if change < 0 and dump_enabled:\n                message += \"{0} *{1} Interval* | Change: _{2:.3f}%_\\n\".format(\n                    self.dump_emoji,\n                    interval,\n                    change * 100,\n                    asset[\"price\"][-1],\n                )\n\n        # Skip alert if change is not big enough to avoid spam\n        if abs(change_biggest_delta) < (self.alert_skip_threshold / 100):\n            self.logger.debug(\n                \"Change for asset: %s on all intervals is to low: %s. Skipping this alert report.\",\n                asset[\"symbol\"],\n                change_biggest_delta,\n            )\n            return\n\n        news_message = \"\"\"\\\n*{0}* | {1} Alert(s) | {2}\n\nPrice: _{3:.10f}_ | Volume: _{4}_\n\n{5}\nOpen in [Binance Spot](https://www.binance.com/en/trade/{0})\\\n            \"\"\".format(\n            asset[\"symbol\"],\n            no_of_alerts,\n            datetime.fromtimestamp(current_time).strftime(\"%Y-%m-%d %H:%M:%S\"),\n            asset[\"price\"][-1],\n            0,\n            message,\n        )\n\n        self.telegram.send_news_message(news_message)\n\n    def send_top_pump_dump_statistics_report(\n        self,\n        assets,\n        interval,\n        top_pump_enabled=True,\n        top_dump_enabled=True,\n        additional_stats_enabled=True,\n        no_of_reported_coins=5,\n    ):\n\n        if not top_pump_enabled or not top_dump_enabled:\n            return\n\n        message = \"*[{0} Interval]*\\n\\n\".format(interval)\n\n        if top_pump_enabled:\n            pump_sorted_list = sorted(\n                assets,\n                key=lambda item: item[interval][\"change_current\"],\n                reverse=True,\n            )[0:no_of_reported_coins]\n\n            message += \"{0} *Top {1} Pumps*\\n\".format(\n                self.pump_emoji, no_of_reported_coins\n            )\n\n            for asset in pump_sorted_list:\n                message += \"- {0}: _{1:.2f}_%\\n\".format(\n                    asset[\"symbol\"], asset[interval][\"change_current\"] * 100\n                )\n            message += \"\\n\"\n\n        if top_dump_enabled:\n            dump_sorted_list = sorted(\n                assets, key=lambda item: item[interval][\"change_current\"]\n            )[0:no_of_reported_coins]\n\n            message += \"{0} *Top {1} Dumps*\\n\".format(\n                self.dump_emoji, no_of_reported_coins\n            )\n\n            for asset in dump_sorted_list:\n                message += \"- {0}: _{1:.2f}_%\\n\".format(\n                    asset[\"symbol\"], asset[interval][\"change_current\"] * 100\n                )\n\n        if additional_stats_enabled:\n            if top_pump_enabled or top_dump_enabled:\n                message += \"\\n\"\n            message += self.generate_additional_statistics_report(assets, interval)\n\n        self.telegram.send_report_message(message, is_alert_chat=True)\n\n    def generate_additional_statistics_report(self, assets, interval):\n        up = 0\n        down = 0\n        sum_change = 0\n\n        for asset in assets:\n            if asset[interval][\"change_current\"] > 0:\n                up += 1\n            elif asset[interval][\"change_current\"] < 0:\n                down += 1\n\n            sum_change += asset[interval][\"change_current\"]\n\n        avg_change = sum_change / len(assets)\n\n        return \"*Average Change:* {0:.2f}%\\n {1} {2} / {3} {4}\".format(\n            avg_change * 100,\n            self.pump_emoji,\n            up,\n            self.dump_emoji,\n            down,\n        )\n"
  },
  {
    "path": "reporter/__init__.py",
    "content": "from .ReportGenerator import ReportGenerator\n"
  },
  {
    "path": "requirements.txt",
    "content": "colorlog\nrequests\npyyaml\npython-telegram-bot==13.13\n"
  },
  {
    "path": "sender/TelegramSender.py",
    "content": "import logging\n\nfrom concurrent.futures import ThreadPoolExecutor\nfrom telegram import Bot, ParseMode\nfrom telegram.error import RetryAfter\nfrom telegram.utils.request import Request\nfrom time import sleep\n\n\nclass TelegramSender:\n    def __init__(\n        self,\n        token,\n        chat_id,\n        alert_chat_id=0,\n        bot_emoji=\"\\U0001F916\",  # 🤖\n        top_emoji=\"\\U0001F3C6\",  # 🏆\n        news_emoji=\"\\U0001F4F0\",  # 📰\n    ):\n\n        self.token = token\n        self.chat_id = chat_id\n        self.alert_chat_id = alert_chat_id\n\n        self.bot_emoji = bot_emoji\n        self.top_emoji = top_emoji\n        self.news_emoji = news_emoji\n\n        self.telegram_executor = ThreadPoolExecutor(max_workers=3)\n\n        self.request = Request(con_pool_size=3)\n        self.bot = Bot(self.token, request=self.request)\n\n        self.logger = logging.getLogger(\"telegram-sender\")\n\n    def is_alert_chat_enabled(self):\n        return self.alert_chat_id != 0 and self.alert_chat_id != self.chat_id\n\n    def send_message(self, message, is_alert_chat=False):\n        chat_id = self.chat_id if not is_alert_chat else self.alert_chat_id\n\n        def push_message(bot, chat_id, message):\n            self.logger.info(message)\n\n            try:\n                bot.send_message(\n                    chat_id=chat_id,\n                    text=message,\n                    parse_mode=ParseMode.MARKDOWN,\n                    disable_web_page_preview=True,\n                )\n            except RetryAfter as e:\n                self.logger.error(\n                    \"Flood limit is exceeded. Sleep {} seconds.\", e.retry_after\n                )\n                sleep(e.retry_after)\n                # Resend message to the queue\n                self.send_message(message, is_alert_chat)\n            except Exception as e:\n                self.logger.error(str(e))\n\n        self.telegram_executor.submit(\n            lambda p: push_message(*p), (self.bot, chat_id, message)\n        )\n\n    def send_generic_message(self, message, args=None, is_alert_chat=False):\n        if args is not None:\n            message = message.format(args)\n        self.send_message(self.bot_emoji + \" \" + message, is_alert_chat)\n\n    def send_report_message(self, message, args=None, is_alert_chat=False):\n        if args is not None:\n            message = message.format(args)\n        self.send_message(self.top_emoji + \" \" + message, is_alert_chat)\n\n    def send_news_message(self, message, args=None, is_alert_chat=False):\n        if args is not None:\n            message = message.format(args)\n        self.send_message(self.news_emoji + \" \" + message, is_alert_chat)\n"
  },
  {
    "path": "sender/__init__.py",
    "content": "from .TelegramSender import TelegramSender\n"
  },
  {
    "path": "utils/ConversionUtils.py",
    "content": "class ConversionUtils:\r\n    @staticmethod\r\n    def duration_to_seconds(duration):\r\n        unit = duration[-1]\r\n        if unit == \"s\":\r\n            unit = 1\r\n        elif unit == \"m\":\r\n            unit = 60\r\n        elif unit == \"h\":\r\n            unit = 3600\r\n\r\n        return int(duration[:-1]) * unit\r\n"
  },
  {
    "path": "utils/__init__.py",
    "content": "from .ConversionUtils import ConversionUtils\n"
  }
]