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