├── requirements.txt ├── setup.py ├── .github ├── FUNDING.yml ├── workflows │ ├── black.yml │ └── tests.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── old_coins.example.json ├── requirements_additional_build_agent.txt ├── dev_requirements.txt ├── main.py ├── src └── gateio_new_coins_announcements_bot │ ├── load_config.py │ ├── auth │ └── gateio_auth.py │ ├── globals.py │ ├── store_order.py │ ├── send_telegram.py │ ├── logger.py │ ├── trade_client.py │ ├── new_listings_scraper.py │ └── main.py ├── tests └── test_new_listings_scraper.py ├── auth └── auth.example.yml ├── Dockerfile ├── pyproject.toml ├── tox.ini ├── setup.cfg ├── LICENSE ├── config.example.yml ├── .pre-commit-config.yaml ├── development_setup.md ├── README.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | requests==2.25.1 3 | gate_api==4.22.2 4 | PyYAML==6.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if __name__ == "__main__": 4 | setup() 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [CyberPunkMetalHead] 4 | -------------------------------------------------------------------------------- /old_coins.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | "ADD_COINS_TO_IGNORE_HERE", 3 | "MAKE_SURE_TO_PUT_COMMAS_BETWEEN_COINS", 4 | "BTC" 5 | ] 6 | -------------------------------------------------------------------------------- /requirements_additional_build_agent.txt: -------------------------------------------------------------------------------- 1 | flake8==3.9.2 2 | tox==3.24.3 3 | pytest==6.2.5 4 | pytest-cov==3.0.0 5 | mypy==0.910 6 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.9.2 2 | tox==3.24.3 3 | pytest==6.2.5 4 | pytest-cov==3.0.0 5 | mypy==0.910 6 | pre-commit==2.15.0 7 | black==21.12b0 8 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from gateio_new_coins_announcements_bot.main import main 2 | 3 | if __name__ == "__main__": 4 | # TODO: refactor this main call 5 | main() 6 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/load_config.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | def load_config(file): 5 | with open(file) as file: 6 | return yaml.load(file, Loader=yaml.FullLoader) 7 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Black Lint Check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: psf/black@stable 11 | with: 12 | options: "--check --verbose" 13 | src: "." 14 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/auth/gateio_auth.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from gate_api import Configuration 3 | 4 | 5 | def load_gateio_creds(file): 6 | with open(file) as file: 7 | auth = yaml.load(file, Loader=yaml.FullLoader) 8 | 9 | return Configuration(key=auth["gateio_api"], secret=auth["gateio_secret"]) 10 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/globals.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | buy_ready = threading.Event() 4 | sell_ready = threading.Event() 5 | stop_threads = False 6 | old_coins = {} 7 | latest_listing = "" 8 | 9 | # TRADE_OPTIONS config values 10 | quantity = 15 11 | pairing = "USDT" 12 | test_mode = True 13 | sl = -3 14 | tp = 2 15 | enable_tsl = True 16 | tsl = -4 17 | ttp = 2 18 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/store_order.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def store_order(file, order): 5 | """ 6 | Save order into local json file 7 | """ 8 | with open(file, "w") as f: 9 | json.dump(order, f, indent=4) 10 | 11 | 12 | def load_order(file): 13 | """ 14 | Update Json file 15 | """ 16 | with open(file, "r+") as f: 17 | return json.load(f) 18 | -------------------------------------------------------------------------------- /tests/test_new_listings_scraper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # import os 4 | # import sys 5 | 6 | # currentdir = os.path.dirname(os.path.realpath(__file__)) 7 | # parentdir = os.path.dirname(currentdir) 8 | # sys.path.append(parentdir) 9 | 10 | 11 | @pytest.mark.skip("Can only be executed locally at this moment") 12 | def test_latest_announcement(): 13 | from gateio_new_coins_announcements_bot.new_listings_scraper import get_announcement 14 | 15 | assert get_announcement() 16 | -------------------------------------------------------------------------------- /auth/auth.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # CLIENT DETAILS - MAKE SURE YOU USE APIv4 KEYS. USING APIv2 KEYS 3 | # WILL CAUSE ERRORS WHEN TRYING TO BUY/SELL 4 | gateio_api: "GATEIO_APIv4_KEY" 5 | gateio_secret: "GATEIO_SECRET_v4" 6 | 7 | # Getting a bot token: https://core.telegram.org/bots#6-botfather 8 | # Getting your chatID: find your bot and send a message, 9 | # then go visit https://api.telegram.org/bot/getUpdates and find the chat ID 10 | telegram_token: "my_token" 11 | telegram_chat_id: "my_chat_id" 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # API Dockerfile 2 | FROM python:3.9-alpine 3 | 4 | # Copy requirements file 5 | COPY requirements.txt . 6 | 7 | # Copy module files 8 | COPY src/gateio_new_coins_announcements_bot ./src/gateio_new_coins_announcements_bot 9 | COPY pyproject.toml . 10 | COPY README.md . 11 | COPY setup.cfg . 12 | COPY setup.py . 13 | COPY main.py . 14 | 15 | # Copy relevant files to run bot 16 | COPY config.yml . 17 | COPY old_coins.json . 18 | COPY auth/auth.yml ./auth/ 19 | 20 | #root directory contains main.py to start the bot as well as all config/auth.yml files 21 | WORKDIR . 22 | 23 | # install necessary requirements 24 | RUN pip3 install -r requirements.txt 25 | 26 | CMD [ "python", "main.py"] 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 46.4.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | addopts = "--cov=gateio_new_coins_announcements_bot" 7 | testpaths = [ 8 | "tests", 9 | ] 10 | 11 | [tool.mypy] 12 | mypy_path = "src" 13 | check_untyped_defs = true 14 | disallow_any_generics = true 15 | ignore_missing_imports = true 16 | no_implicit_optional = true 17 | show_error_codes = true 18 | strict_equality = true 19 | warn_redundant_casts = true 20 | warn_return_any = true 21 | warn_unreachable = true 22 | warn_unused_configs = true 23 | no_implicit_reexport = true 24 | 25 | [tool.black] 26 | target-version = ['py39'] 27 | line-length = 120 28 | include = '\.pyi?$' 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.8.0 3 | envlist = py38, py39, flake8 #, mypy 4 | isolated_build = true 5 | 6 | [gh-actions] 7 | python = 8 | 3.8: py38, flake8 #, mypy 9 | 3.9: py39 10 | 11 | [testenv] 12 | setenv = 13 | PYTHONPATH = {toxinidir} 14 | deps = 15 | -r{toxinidir}/requirements.txt 16 | -r{toxinidir}/requirements_additional_build_agent.txt 17 | commands = 18 | pytest --basetemp={envtmpdir} 19 | 20 | [testenv:flake8] 21 | basepython = python3.8 22 | deps = flake8 23 | commands = flake8 src tests 24 | 25 | # [testenv:mypy] 26 | # basepython = python3.8 27 | # deps = 28 | # -r{toxinidir}/requirements.txt 29 | # -r{toxinidir}/requirements_additional_build_agent.txt 30 | # types-requests>=2.26.0 31 | # commands = mypy src 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests/Flake8 check for Python 3.8, 3.9 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest] 13 | python-version: ['3.8', '3.9'] 14 | defaults: 15 | run: 16 | working-directory: . 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install tox tox-gh-actions 27 | - name: Test with tox 28 | run: tox 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = gateio_new_coins_announcements_bot 3 | long_description = file: README.md 4 | long_description_content_type = text/markdown 5 | license_files = LICENSE 6 | license = 'MIT' 7 | 8 | [options] 9 | install_requires = 10 | requests>=2.25 11 | gate_api>=4.22 12 | PyYAML>=6.0 13 | 14 | packages = 15 | gateio_new_coins_announcements_bot 16 | gateio_new_coins_announcements_bot.auth 17 | python_requires = >=3.8 18 | package_dir = 19 | =src 20 | zip_safe = False 21 | 22 | [options.extras_require] 23 | testing = 24 | pytest>=6.0 25 | pytest-cov>=2.0 26 | flake8>=3.9 27 | tox>=3.24 28 | ; mypy>=0.910 29 | 30 | ; [options.package_data] 31 | ; gateio_new_coins_announcements_bot = py.typed 32 | 33 | [flake8] 34 | max-line-length = 120 35 | ; W503: https://stackoverflow.com/questions/57074300/what-is-the-recommended-way-to-break-long-if-statement-w504-line-break-after-b 36 | extend-ignore = W503 37 | exclude = 38 | .git, 39 | __pycache__, 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Logs** 27 | If applicable, add logs to help explain your problem. 28 | Please use the markdown code block format. 29 | e.g. 30 | ``` 31 | INFO: Getting the list of supported currencies from gate io 32 | INFO: List of gate io currencies saved to currencies.json. Waiting 5 minutes before refreshing list... 33 | INFO: new-coi.... 34 | ``` 35 | 36 | 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | TRADE_OPTIONS: 3 | # QUANTITY and PAIRING determine how much of your wallet value will be used to buy the new token. 4 | # 'QUANTITY: 15' and 'PAIRING: USDT' means you will spend 15 USDT to buy XXX_USDT where XXX is the new token. 5 | KUCOIN_ANNOUNCEMENTS: False 6 | QUANTITY: 15 7 | PAIRING: USDT 8 | # Test mode. No real money will be used. 9 | TEST: True 10 | # Stop Loss in % of your buy value. 11 | SL: -3 12 | # Take Profit in % of your buy value. 13 | TP: 2 14 | # DO NOT DISABLE TSL!!!! YOUR BOT WILL NOT SELL 15 | ENABLE_TSL: True 16 | # Trailing Stop Loss 17 | TSL: -4 18 | # Trailing Take Profit 19 | TTP: 2 20 | LOGGING: 21 | # Logging levels used in this program are ERROR, INFO, and DEBUG 22 | LOG_LEVEL: INFO 23 | LOG_FILE: bot.log 24 | LOG_TO_CONSOLE: True 25 | TELEGRAM: 26 | # set to True to enable telegram notifications 27 | ENABLED: False 28 | # Disable / Enable specific notifications 29 | NOTIFICATIONS: 30 | STARTUP: True # welcome message 31 | COIN_ANNOUNCEMENT: True # detected new announcement 32 | COIN_NOT_SUPPORTED: True # coin is not on gate.io 33 | BUY_START: True # when entering position 34 | BUY_ORDER_CREATED: True # when buy order is created 35 | BUY_FILLED: True # when the buy order got filled 36 | SELL_START: True # when starting to sell 37 | SELL_FILLED: True # when sold 38 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/send_telegram.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import requests 4 | import yaml 5 | 6 | from gateio_new_coins_announcements_bot.load_config import load_config 7 | 8 | config = load_config("config.yml") 9 | 10 | with open("auth/auth.yml") as file: 11 | try: 12 | creds = yaml.load(file, Loader=yaml.FullLoader) 13 | bot_token = creds["telegram_token"] 14 | bot_chatID = str(creds["telegram_chat_id"]) 15 | valid_auth = True 16 | except KeyError: 17 | valid_auth = False 18 | pass 19 | 20 | 21 | class TelegramLogFilter(logging.Filter): 22 | # filter for logRecords with TELEGRAM extra 23 | def filter(self, record): 24 | return hasattr(record, "TELEGRAM") 25 | 26 | 27 | class TelegramHandler(logging.Handler): 28 | # log to telegram if the TELEGRAM extra matches an enabled key 29 | def emit(self, record): 30 | 31 | if not valid_auth: 32 | return 33 | 34 | key = getattr(record, "TELEGRAM") 35 | 36 | # unknown message key 37 | if key not in config["TELEGRAM"]["NOTIFICATIONS"]: 38 | return 39 | 40 | # message key disabled 41 | if not config["TELEGRAM"]["NOTIFICATIONS"][key]: 42 | return 43 | 44 | requests.get( 45 | f"""https://api.telegram.org/bot{bot_token}/sendMessage 46 | ?chat_id={bot_chatID} 47 | &parse_mode=Markdown 48 | &text={record.message}""" 49 | ) 50 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_language_version: 4 | python: python3.8 5 | default_stages: [commit, push] 6 | fail_fast: false 7 | repos: 8 | - repo: https://github.com/psf/black 9 | rev: 21.12b0 10 | hooks: 11 | - id: black 12 | args: [--safe, --config=pyproject.toml] 13 | 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.0.1 16 | hooks: 17 | - id: trailing-whitespace 18 | - id: end-of-file-fixer 19 | - id: check-yaml 20 | - id: check-added-large-files 21 | - id: debug-statements 22 | language_version: python3 23 | 24 | - repo: https://github.com/PyCQA/flake8 25 | rev: 4.0.1 26 | hooks: 27 | - id: flake8 28 | args: [--config=setup.cfg] 29 | language_version: python3 30 | 31 | - repo: https://github.com/asottile/reorder_python_imports 32 | rev: v2.6.0 33 | hooks: 34 | - id: reorder-python-imports 35 | args: [--application-directories=./src, --py37-plus] 36 | 37 | - repo: https://github.com/asottile/pyupgrade 38 | rev: v2.29.1 39 | hooks: 40 | - id: pyupgrade 41 | args: [--py37-plus] 42 | 43 | # Add later, due to typing issues this is currently disabled 44 | # - repo: https://github.com/pre-commit/mirrors-mypy 45 | # rev: v0.920 46 | # hooks: 47 | # - id: mypy 48 | # files: ^src/ 49 | # args: [--no-strict-optional, --ignore-missing-imports] 50 | # additional_dependencies: [] 51 | -------------------------------------------------------------------------------- /development_setup.md: -------------------------------------------------------------------------------- 1 | ## 1. Create venv (recommended to use python>=3.8, check with `python --version`) 2 | 3 | python3 -m venv env 4 | 5 | ## 2 Activate venv 6 | 7 | Linux: 8 | source env/bin/activate 9 | 10 | Windows: 11 | env\Scripts\activate.bat 12 | 13 | ## 3. Install program requirements 14 | 15 | python -m pip install -r requirements.txt 16 | 17 | This contains the requirements for the program itself. 18 | 19 | ## 4. Install dev requirements 20 | 21 | python -m pip install -r dev_requirements.txt 22 | 23 | This is necessary make verifying of the code easier and formats the code automatically to match the coding style. 24 | 25 | ## 5. Install pre-commit hooks 26 | 27 | pre-commit install 28 | 29 | This installs the pre-commit git hooks for the project and makes it possible to run the pre-commit script automatically when committing. 30 | 31 | ## 6. Run Tests and pre-commit scripts manually 32 | ### pre-commit checks 33 | To manually run the pre-commit script: 34 | 35 | pre-commit run --all-files 36 | 37 | ### Tox 38 | Make sure you enabled the virtual environment. 39 | Tox tests the code for multiple environments (3.8, 3.9) and checks code with flake8 and mypy (only on Python Version 3.8). 40 | To run Tox: 41 | 42 | tox 43 | 44 | ### PyTest 45 | Make sure you enabled the virtual environment. 46 | PyTest runs the unit tests for the code. 47 | To run PyTest: 48 | 49 | python -m pytest 50 | 51 | 52 | ### Flake8 53 | Make sure you enabled the virtual environment. 54 | Flake8 checks the code for errors and warnings. 55 | To run Flake8: 56 | 57 | flake8 src 58 | 59 | ### Black 60 | Make sure you enabled the virtual environment. 61 | Black formats the code to match the coding style. 62 | To run Black: 63 | 64 | black src 65 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from logging.handlers import TimedRotatingFileHandler 4 | 5 | from gateio_new_coins_announcements_bot.load_config import load_config 6 | from gateio_new_coins_announcements_bot.send_telegram import TelegramHandler 7 | from gateio_new_coins_announcements_bot.send_telegram import TelegramLogFilter 8 | 9 | # loads local configuration 10 | config = load_config("config.yml") 11 | 12 | log = logging 13 | 14 | # Set default log settings 15 | log_level = "INFO" 16 | cwd = os.getcwd() 17 | log_dir = "logs" 18 | log_file = "bot.log" 19 | log_to_console = True 20 | log_path = os.path.join(cwd, log_dir, log_file) 21 | 22 | # create logging directory 23 | if not os.path.exists(log_dir): 24 | os.mkdir(log_dir) 25 | 26 | # Get logging variables 27 | log_level = config["LOGGING"]["LOG_LEVEL"] 28 | log_file = config["LOGGING"]["LOG_FILE"] 29 | 30 | try: 31 | log_telegram = config["TELEGRAM"]["ENABLED"] 32 | except KeyError: 33 | pass 34 | 35 | try: 36 | log_to_console = config["LOGGING"]["LOG_TO_CONSOLE"] 37 | except KeyError: 38 | pass 39 | 40 | file_handler = TimedRotatingFileHandler(log_path, when="midnight") 41 | handlers = [file_handler] 42 | if log_to_console: 43 | handlers.append(logging.StreamHandler()) 44 | if log_telegram: 45 | telegram_handler = TelegramHandler() 46 | telegram_handler.addFilter(TelegramLogFilter()) # only handle messages with extra: TELEGRAM 47 | telegram_handler.setLevel(logging.NOTSET) # so that telegram can recieve any kind of log message 48 | handlers.append(telegram_handler) 49 | 50 | log.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", handlers=handlers) 51 | 52 | logger = logging.getLogger(__name__) 53 | level = logging.getLevelName(log_level) 54 | logger.setLevel(level) 55 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/trade_client.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from gate_api import ApiClient 4 | from gate_api import Order 5 | from gate_api import SpotApi 6 | 7 | from gateio_new_coins_announcements_bot.auth.gateio_auth import load_gateio_creds 8 | from gateio_new_coins_announcements_bot.logger import logger 9 | 10 | client = load_gateio_creds("auth/auth.yml") 11 | spot_api = SpotApi(ApiClient(client)) 12 | 13 | last_trade = None 14 | 15 | 16 | def get_last_price(base, quote, return_price_only): 17 | """ 18 | Args: 19 | 'DOT', 'USDT' 20 | """ 21 | global last_trade 22 | trades = spot_api.list_trades(currency_pair=f"{base}_{quote}", limit=1) 23 | assert len(trades) == 1 24 | trade = trades[0] 25 | 26 | create_time_ms = datetime.utcfromtimestamp(int(trade.create_time_ms.split(".")[0]) / 1000) 27 | create_time_formatted = create_time_ms.strftime("%d-%m-%y %H:%M:%S.%f") 28 | 29 | if last_trade and last_trade.id > trade.id: 30 | logger.debug("STALE TRADEBOOK RESULT FOUND. RE-TRYING.") 31 | return get_last_price(base=base, quote=quote, return_price_only=return_price_only) 32 | else: 33 | last_trade = trade 34 | 35 | if return_price_only: 36 | return trade.price 37 | 38 | logger.info( 39 | f"LATEST TRADE: {trade.currency_pair} | id={trade.id} | create_time={create_time_formatted} | " 40 | f"side={trade.side} | amount={trade.amount} | price={trade.price}" 41 | ) 42 | return trade 43 | 44 | 45 | def get_min_amount(base, quote): 46 | """ 47 | Args: 48 | 'DOT', 'USDT' 49 | """ 50 | try: 51 | min_amount = spot_api.get_currency_pair(currency_pair=f"{base}_{quote}").min_quote_amount 52 | except Exception as e: 53 | logger.error(e) 54 | else: 55 | return min_amount 56 | 57 | 58 | def place_order(base, quote, amount, side, last_price): 59 | """ 60 | Args: 61 | 'DOT', 'USDT', 50, 'buy', 400 62 | """ 63 | try: 64 | order = Order( 65 | amount=str(float(amount) / float(last_price)), 66 | price=last_price, 67 | side=side, 68 | currency_pair=f"{base}_{quote}", 69 | time_in_force="ioc", 70 | ) 71 | order = spot_api.create_order(order) 72 | t = order 73 | logger.info( 74 | f"PLACE ORDER: {t.side} | {t.id} | {t.account} | {t.type} | {t.currency_pair} | {t.status} | " 75 | f"amount={t.amount} | price={t.price} | left={t.left} | filled_total={t.filled_total} | " 76 | f"fill_price={t.fill_price} | fee={t.fee} {t.fee_currency}" 77 | ) 78 | except Exception as e: 79 | logger.error(e) 80 | raise 81 | 82 | else: 83 | return order 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gateio-trading-bot-binance-announcements 2 | This Gateio x Binance cryptocurrency trading bot scans the Binance Announcements page and picks up on new coin listings. 3 | It then goes to Gateio and places a buy order on the coin that's due to be listed on Binance. 4 | It comes with trailing stop loss, take profit and a test mode. 5 | 6 | The idea behind this open source crypto trading algorithm to take advantage of the price spike of new coins as they are being announced for listing on Binance. 7 | As Gateio seems to list many of these coins before Binance does, this exchange is a good place to start. 8 | It comes with a live and test mode so naturally, use at your own risk. 9 | 10 | # HOW TO RUN IT 11 | ## 1. Create venv (recommended to use python>=3.8, check with `python --version`) 12 | 13 | python3 -m venv env 14 | 15 | ## 2 Activate venv 16 | 17 | Linux: 18 | source env/bin/activate 19 | 20 | Windows: 21 | env\Scripts\activate.bat 22 | 23 | ## 3. Install program requirements 24 | 25 | python -m pip install -r requirements.txt 26 | 27 | This contains the requirements for the program itself. You may now run the script using python main.py. 28 | No additional steps needed for simply running the tool, you may stop here. 29 | 30 | ## 4. Install dev requirements 31 | 32 | python -m pip install -r dev_requirements.txt 33 | 34 | This is necessary make verifying of the code easier and formats the code automatically to match the coding style. 35 | 36 | ## 5. Install pre-commit hooks 37 | 38 | pre-commit install 39 | 40 | This installs the pre-commit git hooks for the project and makes it possible to run the pre-commit script automatically when committing. 41 | 42 | ## 6. Run Tests and pre-commit scripts manually 43 | ### pre-commit checks 44 | To manually run the pre-commit script: 45 | 46 | pre-commit run --all-files 47 | 48 | ### Tox 49 | Make sure you enabled the virtual environment. 50 | Tox tests the code for multiple environments (3.8, 3.9) and checks code with flake8 and mypy (only on Python Version 3.8). 51 | To run Tox: 52 | 53 | tox 54 | 55 | ### PyTest 56 | Make sure you enabled the virtual environment. 57 | PyTest runs the unit tests for the code. 58 | To run PyTest: 59 | 60 | python -m pytest 61 | 62 | 63 | ### Flake8 64 | Make sure you enabled the virtual environment. 65 | Flake8 checks the code for errors and warnings. 66 | To run Flake8: 67 | 68 | flake8 src 69 | 70 | ### Black 71 | Make sure you enabled the virtual environment. 72 | Black formats the code to match the coding style. 73 | To run Black: 74 | 75 | black src 76 | 77 | 78 | 79 |

 

80 | 81 | **For a step-by-step guide on how to set it up and configure please see the guide here:** [Binance new coin trading bot guide](https://www.cryptomaton.org/2021/10/17/a-binance-and-gate-io-crypto-trading-bot-for-new-coin-announcements//) 82 | 83 | 84 |

 

85 | 86 | **The new coins crypto trading bot explained in more detail.
87 | See the video linked below for an explanation and rationale behind the bot.** 88 | 89 | [![binance new coin listings bot](https://img.youtube.com/vi/mIa9eQDhubs/0.jpg)](https://youtu.be/SsSgD0v16Kg) 90 | 91 | Want to talk trading bots? Join the discord [https://discord.gg/Ga56KXUUNn](https://discord.gg/Ga56KXUUNn) 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore creds file 2 | **/auth/auth.yml 3 | 4 | # Ignore orders 5 | **/order.json 6 | **/sold.json 7 | **/new_listing.json 8 | **/session.json 9 | 10 | # Ignore bot log 11 | */bot.log 12 | **/logs 13 | 14 | # Ignore currency list and old coins 15 | **/currencies.json 16 | **/old_coins.json 17 | 18 | # Ignore pycache 19 | **/__pycache__/ 20 | **/auth/__pycache__/ 21 | 22 | # Ignore config 23 | **/config.yml 24 | 25 | # Ignore test file 26 | **/test_order.py 27 | 28 | .idea/ 29 | 30 | # Visual Studio Code as suggested by https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | *.code-workspace 37 | 38 | # Local History for Visual Studio Code 39 | .history/ 40 | 41 | 42 | 43 | # Created by https://www.toptal.com/developers/gitignore/api/python 44 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 45 | 46 | ### Python ### 47 | # Byte-compiled / optimized / DLL files 48 | __pycache__/ 49 | *.py[cod] 50 | *$py.class 51 | 52 | # C extensions 53 | *.so 54 | 55 | # Distribution / packaging 56 | .Python 57 | build/ 58 | develop-eggs/ 59 | dist/ 60 | downloads/ 61 | eggs/ 62 | .eggs/ 63 | lib/ 64 | lib64/ 65 | parts/ 66 | sdist/ 67 | var/ 68 | wheels/ 69 | share/python-wheels/ 70 | *.egg-info/ 71 | .installed.cfg 72 | *.egg 73 | MANIFEST 74 | 75 | # PyInstaller 76 | # Usually these files are written by a python script from a template 77 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 78 | *.manifest 79 | *.spec 80 | 81 | # Installer logs 82 | pip-log.txt 83 | pip-delete-this-directory.txt 84 | 85 | # Unit test / coverage reports 86 | htmlcov/ 87 | .tox/ 88 | .nox/ 89 | .coverage 90 | .coverage.* 91 | .cache 92 | nosetests.xml 93 | coverage.xml 94 | *.cover 95 | *.py,cover 96 | .hypothesis/ 97 | .pytest_cache/ 98 | cover/ 99 | 100 | # Translations 101 | *.mo 102 | *.pot 103 | 104 | # Django stuff: 105 | *.log 106 | local_settings.py 107 | db.sqlite3 108 | db.sqlite3-journal 109 | 110 | # Flask stuff: 111 | instance/ 112 | .webassets-cache 113 | 114 | # Scrapy stuff: 115 | .scrapy 116 | 117 | # Sphinx documentation 118 | docs/_build/ 119 | 120 | # PyBuilder 121 | .pybuilder/ 122 | target/ 123 | 124 | # Jupyter Notebook 125 | .ipynb_checkpoints 126 | 127 | # IPython 128 | profile_default/ 129 | ipython_config.py 130 | 131 | # pyenv 132 | # For a library or package, you might want to ignore these files since the code is 133 | # intended to run in multiple environments; otherwise, check them in: 134 | # .python-version 135 | 136 | # pipenv 137 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 138 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 139 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 140 | # install all needed dependencies. 141 | #Pipfile.lock 142 | 143 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 144 | __pypackages__/ 145 | 146 | # Celery stuff 147 | celerybeat-schedule 148 | celerybeat.pid 149 | 150 | # SageMath parsed files 151 | *.sage.py 152 | 153 | # Environments 154 | .env 155 | .venv 156 | env/ 157 | venv/ 158 | ENV/ 159 | env.bak/ 160 | venv.bak/ 161 | 162 | # Spyder project settings 163 | .spyderproject 164 | .spyproject 165 | 166 | # Rope project settings 167 | .ropeproject 168 | 169 | # mkdocs documentation 170 | /site 171 | 172 | # mypy 173 | .mypy_cache/ 174 | .dmypy.json 175 | dmypy.json 176 | 177 | # Pyre type checker 178 | .pyre/ 179 | 180 | # pytype static type analyzer 181 | .pytype/ 182 | 183 | # Cython debug symbols 184 | cython_debug/ 185 | 186 | # End of https://www.toptal.com/developers/gitignore/api/python 187 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/new_listings_scraper.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | import os.path 4 | import random 5 | import re 6 | import string 7 | import time 8 | 9 | import requests 10 | from gate_api import ApiClient 11 | from gate_api import SpotApi 12 | 13 | import gateio_new_coins_announcements_bot.globals as globals 14 | from gateio_new_coins_announcements_bot.auth.gateio_auth import load_gateio_creds 15 | from gateio_new_coins_announcements_bot.load_config import load_config 16 | from gateio_new_coins_announcements_bot.logger import logger 17 | from gateio_new_coins_announcements_bot.store_order import load_order 18 | 19 | config = load_config("config.yml") 20 | client = load_gateio_creds("auth/auth.yml") 21 | spot_api = SpotApi(ApiClient(client)) 22 | 23 | supported_currencies = None 24 | 25 | previously_found_coins = set() 26 | 27 | 28 | def get_announcement(): 29 | """ 30 | Retrieves new coin listing announcements 31 | 32 | """ 33 | logger.debug("Pulling announcement page") 34 | # Generate random query/params to help prevent caching 35 | rand_page_size = random.randint(1, 200) 36 | letters = string.ascii_letters 37 | random_string = "".join(random.choice(letters) for i in range(random.randint(10, 20))) 38 | random_number = random.randint(1, 99999999999999999999) 39 | queries = [ 40 | "type=1", 41 | "catalogId=48", 42 | "pageNo=1", 43 | f"pageSize={str(rand_page_size)}", 44 | f"rnd={str(time.time())}", 45 | f"{random_string}={str(random_number)}", 46 | ] 47 | random.shuffle(queries) 48 | logger.debug(f"Queries: {queries}") 49 | request_url = ( 50 | f"https://www.binance.com/gateway-api/v1/public/cms/article/list/query" 51 | f"?{queries[0]}&{queries[1]}&{queries[2]}&{queries[3]}&{queries[4]}&{queries[5]}" 52 | ) 53 | 54 | latest_announcement = requests.get(request_url) 55 | if latest_announcement.status_code == 200: 56 | try: 57 | logger.debug(f'X-Cache: {latest_announcement.headers["X-Cache"]}') 58 | except KeyError: 59 | # No X-Cache header was found - great news, we're hitting the source. 60 | pass 61 | 62 | latest_announcement = latest_announcement.json() 63 | logger.debug("Finished pulling announcement page") 64 | return latest_announcement["data"]["catalogs"][0]["articles"][0]["title"] 65 | else: 66 | logger.error(f"Error pulling binance announcement page: {latest_announcement.status_code}") 67 | return "" 68 | 69 | 70 | def get_kucoin_announcement(): 71 | """ 72 | Retrieves new coin listing announcements from Kucoin 73 | 74 | """ 75 | logger.debug("Pulling announcement page") 76 | # Generate random query/params to help prevent caching 77 | rand_page_size = random.randint(1, 200) 78 | letters = string.ascii_letters 79 | random_string = "".join(random.choice(letters) for i in range(random.randint(10, 20))) 80 | random_number = random.randint(1, 99999999999999999999) 81 | queries = [ 82 | "page=1", 83 | f"pageSize={str(rand_page_size)}", 84 | "category=listing", 85 | "lang=en_US", 86 | f"rnd={str(time.time())}", 87 | f"{random_string}={str(random_number)}", 88 | ] 89 | random.shuffle(queries) 90 | logger.debug(f"Queries: {queries}") 91 | request_url = ( 92 | f"https://www.kucoin.com/_api/cms/articles?" 93 | f"?{queries[0]}&{queries[1]}&{queries[2]}&{queries[3]}&{queries[4]}&{queries[5]}" 94 | ) 95 | latest_announcement = requests.get(request_url) 96 | if latest_announcement.status_code == 200: 97 | try: 98 | logger.debug(f'X-Cache: {latest_announcement.headers["X-Cache"]}') 99 | except KeyError: 100 | # No X-Cache header was found - great news, we're hitting the source. 101 | pass 102 | 103 | latest_announcement = latest_announcement.json() 104 | logger.debug("Finished pulling announcement page") 105 | return latest_announcement["items"][0]["title"] 106 | else: 107 | logger.error(f"Error pulling kucoin announcement page: {latest_announcement.status_code}") 108 | return "" 109 | 110 | 111 | def get_last_coin(): 112 | """ 113 | Returns new Symbol when appropriate 114 | """ 115 | # scan Binance Announcement 116 | latest_announcement = get_announcement() 117 | 118 | # enable Kucoin Announcements if True in config 119 | if config["TRADE_OPTIONS"]["KUCOIN_ANNOUNCEMENTS"]: 120 | logger.info("Kucoin announcements enabled, look for new Kucoin coins...") 121 | kucoin_announcement = get_kucoin_announcement() 122 | kucoin_coin = re.findall(r"\(([^)]+)", kucoin_announcement) 123 | 124 | found_coin = re.findall(r"\(([^)]+)", latest_announcement) 125 | uppers = None 126 | 127 | # returns nothing if it's an old coin or it's not an actual coin listing 128 | if ( 129 | "Will List" not in latest_announcement 130 | or found_coin[0] == globals.latest_listing 131 | or found_coin[0] in previously_found_coins 132 | ): 133 | 134 | # if the latest Binance announcement is not a new coin listing, 135 | # or the listing has already been returned, check kucoin 136 | if ( 137 | config["TRADE_OPTIONS"]["KUCOIN_ANNOUNCEMENTS"] 138 | and "Gets Listed" in kucoin_announcement 139 | and kucoin_coin[0] != globals.latest_listing 140 | and kucoin_coin[0] not in previously_found_coins 141 | ): 142 | if len(kucoin_coin) == 1: 143 | uppers = kucoin_coin[0] 144 | previously_found_coins.add(uppers) 145 | logger.info("New Kucoin coin detected: " + uppers) 146 | if len(kucoin_coin) != 1: 147 | uppers = None 148 | 149 | else: 150 | if len(found_coin) == 1: 151 | uppers = found_coin[0] 152 | previously_found_coins.add(uppers) 153 | logger.info("New coin detected: " + uppers) 154 | if len(found_coin) != 1: 155 | uppers = None 156 | 157 | return uppers 158 | 159 | 160 | def store_new_listing(listing): 161 | """ 162 | Only store a new listing if different from existing value 163 | """ 164 | if listing and not listing == globals.latest_listing: 165 | logger.info("New listing detected") 166 | globals.latest_listing = listing 167 | globals.buy_ready.set() 168 | 169 | 170 | def search_and_update(): 171 | """ 172 | Pretty much our main func 173 | """ 174 | while not globals.stop_threads: 175 | sleep_time = 3 176 | for x in range(sleep_time): 177 | time.sleep(1) 178 | if globals.stop_threads: 179 | break 180 | try: 181 | latest_coin = get_last_coin() 182 | if latest_coin: 183 | store_new_listing(latest_coin) 184 | elif globals.test_mode and os.path.isfile("test_new_listing.json"): 185 | store_new_listing(load_order("test_new_listing.json")) 186 | if os.path.isfile("test_new_listing.json.used"): 187 | os.remove("test_new_listing.json.used") 188 | os.rename("test_new_listing.json", "test_new_listing.json.used") 189 | logger.info(f"Checking for coin announcements every {str(sleep_time)} seconds (in a separate thread)") 190 | except Exception as e: 191 | logger.info(e) 192 | else: 193 | logger.info("while loop in search_and_update() has stopped.") 194 | 195 | 196 | def get_all_currencies(single=False): 197 | """ 198 | Get a list of all currencies supported on gate io 199 | :return: 200 | """ 201 | global supported_currencies 202 | while not globals.stop_threads: 203 | logger.info("Getting the list of supported currencies from gate io") 204 | all_currencies = ast.literal_eval(str(spot_api.list_currencies())) 205 | currency_list = [currency["currency"] for currency in all_currencies] 206 | with open("currencies.json", "w") as f: 207 | json.dump(currency_list, f, indent=4) 208 | logger.info( 209 | "List of gate io currencies saved to currencies.json. Waiting 5 " "minutes before refreshing list..." 210 | ) 211 | supported_currencies = currency_list 212 | if single: 213 | return supported_currencies 214 | else: 215 | for x in range(300): 216 | time.sleep(1) 217 | if globals.stop_threads: 218 | break 219 | else: 220 | logger.info("while loop in get_all_currencies() has stopped.") 221 | 222 | 223 | def load_old_coins(): 224 | if os.path.isfile("old_coins.json"): 225 | with open("old_coins.json") as json_file: 226 | data = json.load(json_file) 227 | logger.debug("Loaded old_coins from file") 228 | return data 229 | else: 230 | return [] 231 | 232 | 233 | def store_old_coins(old_coin_list): 234 | with open("old_coins.json", "w") as f: 235 | json.dump(old_coin_list, f, indent=2) 236 | logger.debug("Wrote old_coins to file") 237 | -------------------------------------------------------------------------------- /src/gateio_new_coins_announcements_bot/main.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os.path 3 | import threading 4 | import time 5 | from datetime import datetime 6 | 7 | import gateio_new_coins_announcements_bot.globals as globals 8 | from gateio_new_coins_announcements_bot.load_config import load_config 9 | from gateio_new_coins_announcements_bot.logger import logger 10 | from gateio_new_coins_announcements_bot.new_listings_scraper import get_all_currencies 11 | from gateio_new_coins_announcements_bot.new_listings_scraper import get_last_coin 12 | from gateio_new_coins_announcements_bot.new_listings_scraper import load_old_coins 13 | from gateio_new_coins_announcements_bot.new_listings_scraper import search_and_update 14 | from gateio_new_coins_announcements_bot.new_listings_scraper import store_old_coins 15 | from gateio_new_coins_announcements_bot.store_order import load_order 16 | from gateio_new_coins_announcements_bot.store_order import store_order 17 | from gateio_new_coins_announcements_bot.trade_client import get_last_price 18 | from gateio_new_coins_announcements_bot.trade_client import place_order 19 | 20 | # To add a coin to ignore, add it to the json array in old_coins.json 21 | globals.old_coins = load_old_coins() 22 | logger.debug(f"old_coins: {globals.old_coins}") 23 | 24 | # loads local configuration 25 | config = load_config("config.yml") 26 | 27 | # load necessary files 28 | if os.path.isfile("sold.json"): 29 | sold_coins = load_order("sold.json") 30 | else: 31 | sold_coins = {} 32 | 33 | if os.path.isfile("order.json"): 34 | order = load_order("order.json") 35 | else: 36 | order = {} 37 | 38 | # memory store for all orders for a specific coin 39 | if os.path.isfile("session.json"): 40 | session = load_order("session.json") 41 | else: 42 | session = {} 43 | 44 | # Keep the supported currencies loaded in RAM so no time is wasted fetching 45 | # currencies.json from disk when an announcement is made 46 | logger.debug("Starting get_all_currencies") 47 | supported_currencies = get_all_currencies(single=True) 48 | logger.debug("Finished get_all_currencies") 49 | 50 | logger.info("new-coin-bot online", extra={"TELEGRAM": "STARTUP"}) 51 | 52 | 53 | def buy(): 54 | while not globals.stop_threads: 55 | logger.debug("Waiting for buy_ready event") 56 | globals.buy_ready.wait() 57 | logger.debug("buy_ready event triggered") 58 | if globals.stop_threads: 59 | break 60 | announcement_coin = globals.latest_listing 61 | 62 | global supported_currencies 63 | if ( 64 | announcement_coin 65 | and announcement_coin not in order 66 | and announcement_coin not in sold_coins 67 | and announcement_coin not in globals.old_coins 68 | ): 69 | 70 | logger.info(f"New announcement detected: {announcement_coin}", extra={"TELEGRAM": "COIN_ANNOUNCEMENT"}) 71 | 72 | if not supported_currencies: 73 | supported_currencies = get_all_currencies(single=True) 74 | if supported_currencies: 75 | if announcement_coin in supported_currencies: 76 | logger.debug("Starting get_last_price") 77 | 78 | # get latest price object 79 | obj = get_last_price(announcement_coin, globals.pairing, False) 80 | price = obj.price 81 | 82 | if float(price) <= 0: 83 | continue # wait for positive price 84 | 85 | if announcement_coin not in session: 86 | session[announcement_coin] = {} 87 | session[announcement_coin].update({"total_volume": 0}) 88 | session[announcement_coin].update({"total_amount": 0}) 89 | session[announcement_coin].update({"total_fees": 0}) 90 | session[announcement_coin]["orders"] = list() 91 | 92 | # initalize order object 93 | if announcement_coin not in order: 94 | volume = globals.quantity - session[announcement_coin]["total_volume"] 95 | 96 | order[announcement_coin] = {} 97 | order[announcement_coin]["_amount"] = f"{volume / float(price)}" 98 | order[announcement_coin]["_left"] = f"{volume / float(price)}" 99 | order[announcement_coin]["_fee"] = f"{0}" 100 | order[announcement_coin]["_tp"] = f"{0}" 101 | order[announcement_coin]["_sl"] = f"{0}" 102 | order[announcement_coin]["_status"] = "unknown" 103 | if announcement_coin in session: 104 | if len(session[announcement_coin]["orders"]) == 0: 105 | order[announcement_coin]["_status"] = "test_partial_fill_order" 106 | else: 107 | order[announcement_coin]["_status"] = "cancelled" 108 | 109 | amount = float(order[announcement_coin]["_amount"]) 110 | left = float(order[announcement_coin]["_left"]) 111 | status = order[announcement_coin]["_status"] 112 | 113 | if left - amount != 0: 114 | # partial fill. 115 | amount = left 116 | 117 | logger.info( 118 | f"starting buy place_order with : {announcement_coin=} | {globals.pairing=} | {volume=} | " 119 | + f"{amount=} x {price=} | side = buy | {status=}", 120 | extra={"TELEGRAM": "BUY_START"}, 121 | ) 122 | 123 | try: 124 | # Run a test trade if true 125 | if globals.test_mode: 126 | if order[announcement_coin]["_status"] == "cancelled": 127 | status = "closed" 128 | left = 0 129 | fee = f"{float(amount) * .002}" 130 | else: 131 | status = "cancelled" 132 | left = f"{amount *.66}" 133 | fee = f"{float(amount - float(left)) * .002}" 134 | 135 | order[announcement_coin] = { 136 | "_fee_currency": announcement_coin, 137 | "_price": f"{price}", 138 | "_amount": f"{amount}", 139 | "_time": datetime.timestamp(datetime.now()), 140 | "_tp": globals.tp, 141 | "_sl": globals.sl, 142 | "_ttp": globals.ttp, 143 | "_tsl": globals.tsl, 144 | "_id": "test-order", 145 | "_text": "test-order", 146 | "_create_time": datetime.timestamp(datetime.now()), 147 | "_update_time": datetime.timestamp(datetime.now()), 148 | "_currency_pair": f"{announcement_coin}_{globals.pairing}", 149 | "_status": status, 150 | "_type": "limit", 151 | "_account": "spot", 152 | "_side": "buy", 153 | "_iceberg": "0", 154 | "_left": f"{left}", 155 | "_fee": fee, 156 | } 157 | logger.info("PLACING TEST ORDER") 158 | logger.info(order[announcement_coin]) 159 | # place a live order if False 160 | else: 161 | # just in case...stop buying more than our config amount 162 | assert amount * float(price) <= float(volume) 163 | 164 | order[announcement_coin] = place_order( 165 | announcement_coin, globals.pairing, volume, "buy", price 166 | ) 167 | order[announcement_coin] = order[announcement_coin].__dict__ 168 | order[announcement_coin].pop("local_vars_configuration") 169 | order[announcement_coin]["_tp"] = globals.tp 170 | order[announcement_coin]["_sl"] = globals.sl 171 | order[announcement_coin]["_ttp"] = globals.ttp 172 | order[announcement_coin]["_tsl"] = globals.tsl 173 | logger.debug("Finished buy place_order") 174 | 175 | except Exception as e: 176 | logger.error(e) 177 | 178 | else: 179 | order_status = order[announcement_coin]["_status"] 180 | 181 | logger.info( 182 | f"Order created on {announcement_coin=} at a price of {price} each. {order_status=}", 183 | extra={"TELEGRAM": "BUY_ORDER_CREATED"}, 184 | ) 185 | 186 | if order_status == "closed": 187 | order[announcement_coin]["_amount_filled"] = order[announcement_coin]["_amount"] 188 | session[announcement_coin]["total_volume"] += float( 189 | order[announcement_coin]["_amount"] 190 | ) * float(order[announcement_coin]["_price"]) 191 | session[announcement_coin]["total_amount"] += float(order[announcement_coin]["_amount"]) 192 | session[announcement_coin]["total_fees"] += float(order[announcement_coin]["_fee"]) 193 | session[announcement_coin]["orders"].append(copy.deepcopy(order[announcement_coin])) 194 | 195 | # update order to sum all amounts and all fees 196 | # this will set up our sell order for sale of all filled buy orders 197 | tf = session[announcement_coin]["total_fees"] 198 | ta = session[announcement_coin]["total_amount"] 199 | order[announcement_coin]["_fee"] = f"{tf}" 200 | order[announcement_coin]["_amount"] = f"{ta}" 201 | 202 | store_order("order.json", order) 203 | store_order("session.json", session) 204 | 205 | # We're done. Stop buying and finish up the selling. 206 | globals.sell_ready.set() 207 | globals.buy_ready.clear() 208 | 209 | logger.info(f"Order on {announcement_coin} closed", extra={"TELEGRAM": "BUY_FILLED"}) 210 | else: 211 | if ( 212 | order_status == "cancelled" 213 | and float(order[announcement_coin]["_amount"]) 214 | > float(order[announcement_coin]["_left"]) 215 | and float(order[announcement_coin]["_left"]) > 0 216 | ): 217 | # partial order. Change qty and fee_total in order and finish any remaining balance 218 | partial_amount = float(order[announcement_coin]["_amount"]) - float( 219 | order[announcement_coin]["_left"] 220 | ) 221 | partial_fee = float(order[announcement_coin]["_fee"]) 222 | order[announcement_coin]["_amount_filled"] = f"{partial_amount}" 223 | session[announcement_coin]["total_volume"] += partial_amount * float( 224 | order[announcement_coin]["_price"] 225 | ) 226 | session[announcement_coin]["total_amount"] += partial_amount 227 | session[announcement_coin]["total_fees"] += partial_fee 228 | 229 | session[announcement_coin]["orders"].append(copy.deepcopy(order[announcement_coin])) 230 | 231 | logger.info( 232 | f"Partial fill order detected. {order_status=} | " 233 | + f"{partial_amount=} out of {amount=} | {partial_fee=} | {price=}" 234 | ) 235 | # FUTURE: We'll probably want to start attempting to sell in the future 236 | # immediately after ordering any amount 237 | # It would require at least a minor refactor, since order is getting cleared and 238 | # it seems that this function depends on order being empty, but sell() 239 | # depends on order not being empty. 240 | # globals.sell_ready.set() 241 | 242 | # order not filled, try again. 243 | logger.info(f"Clearing order with a status of {order_status}. Waiting for 'closed' status") 244 | order.pop(announcement_coin) # reset for next iteration 245 | else: 246 | logger.warning( 247 | f"{announcement_coin=} is not supported on gate io", extra={"TELEGRAM": "COIN_NOT_SUPPORTED"} 248 | ) 249 | logger.info(f"Adding {announcement_coin} to old_coins.json") 250 | globals.old_coins.append(announcement_coin) 251 | store_old_coins(globals.old_coins) 252 | else: 253 | logger.error("supported_currencies is not initialized") 254 | else: 255 | logger.info( 256 | "No coins announced, or coin has already been bought/sold. " 257 | + "Checking more frequently in case TP and SL need updating" 258 | ) 259 | time.sleep(3) 260 | 261 | 262 | def sell(): 263 | while not globals.stop_threads: 264 | logger.debug("Waiting for sell_ready event") 265 | globals.sell_ready.wait() 266 | logger.debug("sell_ready event triggered") 267 | if globals.stop_threads: 268 | break 269 | # check if the order file exists and load the current orders 270 | # basically the sell block and update TP and SL logic 271 | if len(order) > 0: 272 | for coin in list(order): 273 | 274 | if float(order[coin]["_tp"]) == 0: 275 | st = order[coin]["_status"] 276 | logger.info(f"Order is initialized but not ready. Continuing. | Status={st}") 277 | continue 278 | 279 | # store some necessary trade info for a sell 280 | coin_tp = order[coin]["_tp"] 281 | coin_sl = order[coin]["_sl"] 282 | 283 | volume = order[coin]["_amount"] 284 | stored_price = float(order[coin]["_price"]) 285 | symbol = order[coin]["_fee_currency"] 286 | 287 | # avoid div by zero error 288 | if float(stored_price) == 0: 289 | continue 290 | 291 | logger.debug( 292 | f"Data for sell: {coin=} | {stored_price=} | {coin_tp=} | {coin_sl=} | {volume=} | {symbol=} " 293 | ) 294 | 295 | logger.info(f"get_last_price existing coin: {coin}") 296 | obj = get_last_price(symbol, globals.pairing, False) 297 | last_price = obj.price 298 | logger.info("Finished get_last_price") 299 | 300 | top_position_price = stored_price + (stored_price * coin_tp / 100) 301 | stop_loss_price = stored_price + (stored_price * coin_sl / 100) 302 | 303 | # need positive price or continue and wait 304 | if float(last_price) == 0: 305 | continue 306 | 307 | logger.info( 308 | f'{symbol=}-{last_price=}\t[STOP: ${"{:,.5f}".format(stop_loss_price)} or' 309 | + f' {"{:,.2f}".format(coin_sl)}%]\t[TOP: ${"{:,.5f}".format(top_position_price)} or' 310 | + f' {"{:,.2f}".format(coin_tp)}%]\t[BUY: ${"{:,.5f}".format(stored_price)} ' 311 | + f'(+/-): {"{:,.2f}".format(((float(last_price) - stored_price) / stored_price) * 100)}%]' 312 | ) 313 | 314 | # update stop loss and take profit values if threshold is reached 315 | if float(last_price) > stored_price + (stored_price * coin_tp / 100) and globals.enable_tsl: 316 | # increase as absolute value for TP 317 | new_tp = float(last_price) + (float(last_price) * globals.ttp / 100) 318 | # convert back into % difference from when the coin was bought 319 | new_tp = float((new_tp - stored_price) / stored_price * 100) 320 | 321 | # same deal as above, only applied to trailing SL 322 | new_sl = float(last_price) + (float(last_price) * globals.tsl / 100) 323 | new_sl = float((new_sl - stored_price) / stored_price * 100) 324 | 325 | # new values to be added to the json file 326 | order[coin]["_tp"] = new_tp 327 | order[coin]["_sl"] = new_sl 328 | store_order("order.json", order) 329 | 330 | new_top_position_price = stored_price + (stored_price * new_tp / 100) 331 | new_stop_loss_price = stored_price + (stored_price * new_sl / 100) 332 | 333 | logger.info(f'updated tp: {round(new_tp, 3)}% / ${"{:,.3f}".format(new_top_position_price)}') 334 | logger.info(f'updated sl: {round(new_sl, 3)}% / ${"{:,.3f}".format(new_stop_loss_price)}') 335 | 336 | # close trade if tsl is reached or trail option is not enabled 337 | elif ( 338 | float(last_price) < stored_price + (stored_price * coin_sl / 100) 339 | or float(last_price) > stored_price + (stored_price * coin_tp / 100) 340 | and not globals.enable_tsl 341 | ): 342 | try: 343 | fees = float(order[coin]["_fee"]) 344 | sell_volume_adjusted = float(volume) - fees 345 | 346 | logger.info( 347 | f"starting sell place_order with :{symbol} | {globals.pairing} | {volume} | " 348 | + f"{sell_volume_adjusted} | {fees} | {float(sell_volume_adjusted)*float(last_price)} | " 349 | + f"side=sell | last={last_price}", 350 | extra={"TELEGRAM": "SELL_START"}, 351 | ) 352 | 353 | # sell for real if test mode is set to false 354 | if not globals.test_mode: 355 | sell = place_order( 356 | symbol, 357 | globals.pairing, 358 | float(sell_volume_adjusted) * float(last_price), 359 | "sell", 360 | last_price, 361 | ) 362 | logger.info("Finish sell place_order") 363 | 364 | # check for completed sell order 365 | if sell._status != "closed": 366 | 367 | # change order to sell remaining 368 | if float(sell._left) > 0 and float(sell._amount) > float(sell._left): 369 | # adjust down order _amount and _fee 370 | order[coin]["_amount"] = sell._left 371 | order[coin]["_fee"] = f"{fees - (float(sell._fee) / float(sell._price))}" 372 | 373 | # add sell order sold.json (handled better in session.json now) 374 | 375 | id = f"{coin}_{sell.id}" 376 | sold_coins[id] = sell 377 | sold_coins[id] = sell.__dict__ 378 | sold_coins[id].pop("local_vars_configuration") 379 | logger.info( 380 | f"Sell order did not close! {sell._left} of {coin} remaining." 381 | + " Adjusted order _amount and _fee to perform sell of remaining balance" 382 | ) 383 | 384 | # add to session orders 385 | try: 386 | if len(session) > 0: 387 | dp = copy.deepcopy(sold_coins[id]) 388 | session[coin]["orders"].append(dp) 389 | except Exception as e: 390 | print(e) 391 | pass 392 | 393 | # keep going. Not finished until status is 'closed' 394 | continue 395 | 396 | logger.info( 397 | f"sold {coin} with {round((float(last_price) - stored_price) * float(volume), 3)} profit" 398 | + f" | {round((float(last_price) - stored_price) / float(stored_price)*100, 3)}% PNL", 399 | extra={"TELEGRAM": "SELL_FILLED"}, 400 | ) 401 | 402 | # remove order from json file 403 | order.pop(coin) 404 | store_order("order.json", order) 405 | logger.debug("Order saved in order.json") 406 | globals.sell_ready.clear() 407 | 408 | except Exception as e: 409 | logger.error(e) 410 | 411 | # store sold trades data 412 | else: 413 | if not globals.test_mode: 414 | sold_coins[coin] = sell 415 | sold_coins[coin] = sell.__dict__ 416 | sold_coins[coin].pop("local_vars_configuration") 417 | sold_coins[coin]["profit"] = f"{float(last_price) - stored_price}" 418 | sold_coins[coin][ 419 | "relative_profit_%" 420 | ] = f"{(float(last_price) - stored_price) / stored_price * 100}%" 421 | 422 | else: 423 | sold_coins[coin] = { 424 | "symbol": coin, 425 | "price": last_price, 426 | "volume": volume, 427 | "time": datetime.timestamp(datetime.now()), 428 | "profit": f"{float(last_price) - stored_price}", 429 | "relative_profit_%": f"{(float(last_price) - stored_price) / stored_price * 100}%", 430 | "id": "test-order", 431 | "text": "test-order", 432 | "create_time": datetime.timestamp(datetime.now()), 433 | "update_time": datetime.timestamp(datetime.now()), 434 | "currency_pair": f"{symbol}_{globals.pairing}", 435 | "status": "closed", 436 | "type": "limit", 437 | "account": "spot", 438 | "side": "sell", 439 | "iceberg": "0", 440 | } 441 | 442 | logger.info(f"Sold coins:\r\n {sold_coins[coin]}") 443 | 444 | # add to session orders 445 | try: 446 | if len(session) > 0: 447 | dp = copy.deepcopy(sold_coins[coin]) 448 | session[coin]["orders"].append(dp) 449 | store_order("session.json", session) 450 | logger.debug("Session saved in session.json") 451 | except Exception as e: 452 | print(e) 453 | pass 454 | 455 | store_order("sold.json", sold_coins) 456 | logger.info("Order saved in sold.json") 457 | else: 458 | logger.debug("Size of order is 0") 459 | time.sleep(3) 460 | 461 | 462 | def main(): 463 | """ 464 | Sells, adjusts TP and SL according to trailing values 465 | and buys new coins 466 | """ 467 | 468 | # Protection from stale announcement 469 | latest_coin = get_last_coin() 470 | if latest_coin: 471 | globals.latest_listing = latest_coin 472 | 473 | # store config deets 474 | globals.quantity = config["TRADE_OPTIONS"]["QUANTITY"] 475 | globals.tp = config["TRADE_OPTIONS"]["TP"] 476 | globals.sl = config["TRADE_OPTIONS"]["SL"] 477 | globals.enable_tsl = config["TRADE_OPTIONS"]["ENABLE_TSL"] 478 | globals.tsl = config["TRADE_OPTIONS"]["TSL"] 479 | globals.ttp = config["TRADE_OPTIONS"]["TTP"] 480 | globals.pairing = config["TRADE_OPTIONS"]["PAIRING"] 481 | globals.test_mode = config["TRADE_OPTIONS"]["TEST"] 482 | 483 | globals.stop_threads = False 484 | globals.buy_ready.clear() 485 | 486 | if not globals.test_mode: 487 | logger.info("!!! LIVE MODE !!!") 488 | 489 | t_get_currencies_thread = threading.Thread(target=get_all_currencies) 490 | t_get_currencies_thread.start() 491 | t_buy_thread = threading.Thread(target=buy) 492 | t_buy_thread.start() 493 | t_sell_thread = threading.Thread(target=sell) 494 | t_sell_thread.start() 495 | 496 | try: 497 | search_and_update() 498 | except KeyboardInterrupt: 499 | logger.info("Stopping Threads") 500 | globals.stop_threads = True 501 | globals.buy_ready.set() 502 | globals.sell_ready.set() 503 | t_get_currencies_thread.join() 504 | t_buy_thread.join() 505 | t_sell_thread.join() 506 | 507 | 508 | if __name__ == "__main__": 509 | logger.info("started working...") 510 | main() 511 | logger.info("stopped working...") 512 | --------------------------------------------------------------------------------