diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..652aa07 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# CODEOWNERS for Bankless ETH Buying Bot + +# Catch all for unspecified items +# This is listed first as CODEOWNERS is chained bottom to top +* @ahrenstein diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..147ad81 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# Supported methods of funding this project: + +github: ahrenstein diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..417dd44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,42 @@ +--- +name: Bug Report +about: Create an issue regarding a bug +title: '' +labels: 'bug' +assignees: '' +--- + +Environment +----------- +* Host OS: +* Docker Version: + +Problem Description +------------------- +``` + +``` + +Steps to Duplicate +------------------ +``` + +``` + +Expected Results +---------------- +``` + +``` + +Actual Results +-------------- +``` + +``` + +Relevant logs +------------- +``` + +``` diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..0a0ec83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,36 @@ +--- +name: Feature Request +about: Request a new feature +title: '' +labels: 'feature' +assignees: '' +--- + +Environment +----------- +* Host OS: +* Docker Version: + +Is your feature request related to a problem? +--------------------------------------------- +``` +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +``` + +Describe the solution you'd like +-------------------------------- +``` +A clear and concise description of what you want to happen. +``` + +Describe alternatives you've considered +--------------------------------------- +``` +A clear and concise description of any alternative solutions or features you've considered. +``` + +Additional context +------------------ +``` +Add any other context or screenshots about the feature request here. +``` diff --git a/.github/ISSUE_TEMPLATE/security-issue.md b/.github/ISSUE_TEMPLATE/security-issue.md new file mode 100644 index 0000000..04c8a69 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security-issue.md @@ -0,0 +1,24 @@ +--- +name: Security Issue +about: Create an issue regarding security holes or bugs +title: '' +labels: 'security' +assignees: '' +--- + +Environment +----------- +* Host OS: +* Docker Version: + +What security issue have you noticed? +------------------------------------- +``` + +``` + +Relevant logs +------------- +``` + +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..67597f7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,38 @@ +Pull Request +============ +What type of Pull Request is this? +- [ ] Bug Report +- [ ] Feature Request +- [ ] Security Issue + +Related Issue: #123456 + +Short Description of original issue +----------------------------------- +``` + +``` + +How this PR resolves it +----------------------- +``` + +``` + +Related Testing +--------------- +``` + +``` + +List of changes +--------------- +``` + +``` + +Additional Notes +---------------- +``` + +``` diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..30fc7c5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,16 @@ +name: 'Testing' + +on: pull_request + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + - name: Install pylint + run: pip install pylint + - name: Run pre-commit + uses: pre-commit/action@v2.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f26c07d --- /dev/null +++ b/.gitignore @@ -0,0 +1,253 @@ +################ +## Sublime Text +################ + +*.sublime-workspace + +################# +## JetBrain IDEs +################# + +## Directory-based project format +.idea/ +# if you remove the above rule, at least ignore user-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# and these sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml + +## File-based project format +*.ipr +*.iml +*.iws + +## Additional for IntelliJ +out/ + +# generated by mpeltonen/sbt-idea plugin +.idea_modules/ + +# generated by JIRA plugin +atlassian-ide-plugin.xml + +# generated by Crashlytics plugin (for Android Studio and Intellij) +com_crashlytics_export_strings.xml + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############### +## Common Keys +############### + +# Ansible vault key +vault.key + +#################### +## Windows detritus +#################### + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +################ +## Mac detritus +################ + +.DS_Store + +################# +## Chef detritus +################# + +# Berkshelf +.vagrant +/cookbooks +Berksfile.lock + +# Bundler +Gemfile.lock +bin/* +.bundle/* + +.kitchen/ +.kitchen.local.yml +inspec.lock + +#################### +## Ansible detritus +#################### + +#Retry files +*.retry + +###################### +## Terraform detritus +###################### + +# Local Terraform states +.terraform/ + + +#################### +## Generic detritus +#################### + +*~ +*# +.#* +\#*# +.*.sw[a-z] +*.un~ +pkg/ + +#################### +## Python detritus +#################### + +# Virtual environments +pyenv/ +venv/ +__pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4661ac6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.3.0 + hooks: + - id: end-of-file-fixer + - id: check-added-large-files + - repo: local + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: + - --ignore-imports=yes + - --fail-under=9 + - --disable=E0401,E1101,W0511 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e2f0d85 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +Crypto Dip Buying Bot: Changelog +================================ +A list of all the changes made to this repo, and the bot it contains + +Version 0.1.0 +------------- + +1. Initial Pre-release of repository + +Return to [README](README.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..df3050c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +Crypto Dip Buying Bot: How To Contribute +======================================== +If you would like to help contribute to this bot you can with a few simple steps. + +1. Fork the repository in GitHub. +2. Add your name to the header of any files you contribute to. +3. Make sure all of your changes are [tested](TESTING.md). +4. List your changes in the [Changelog](CHANGELOG.md). +5. Make an issue stating what you are accomplishing via these changes. +6. Make a pull request against the original repository and link the issue. + +Return to [README](README.md) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1372a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE +# + +FROM python:3.8 +LABEL maintainer = "Matthew Ahrenstein " + +# Copy the source code and poetry config to /app +COPY ./SourceCode/ /app +COPY pyproject.toml /app/ + +# Configure the Python environment using poetry +WORKDIR /app +ENV PYTHONPATH=${PYTHONPATH}:${PWD} +RUN pip3 install poetry +RUN poetry config virtualenvs.create false +RUN poetry install --no-dev + +# Make sure logging to stdout works +ENV PYTHONUNBUFFERED=0 + +# Run the bot +CMD ["python", "-u", "/app/bankless_bot.py", "-c", "/config/config.json"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f060e52 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Matthew Ahrenstein + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..315e58f --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +Crypto Dip Buying Bot +===================== +This bot is designed to buy cryptocurrency on Coinbase Pro using a USD prefunded portfolio whenever it detects a significant dip in price. + +PRE-RELEASE WARNING +------------------- +I tested this code in a sandbox for a few days, before releasing it. I am running this bot against my live Coinbase Pro account as well. +This is still a very new bot with limited testing so **USE THIS BOT AT YOUR OWN RISK!** + +Dip Detection +------------- +The bot runs in hourly cycles. Each cycle the bot will check the price of the specified cryptocurrency. +It will then compare the average price of the previous 7 days worth of price history to the configured dip percentage. +If the current price is the configured percentage lower than the price average it will buy the cryptocurrency in the +specified amount of USD. + +Running The Bot +--------------- +To run the bot you will need Docker and docker-compose installed on your computer. + + docker-compose up -d + +Config File +----------- +You will need the following: + +1. Coinbase Pro credentials tied to the portfolio you want to run the bot against +2. Dip logic parameters: + 1. The cryptocurrency you want to transact in. (It must support being paired against USD in Coinbase Pro) + 2. The buy amount you want in $USD. + 3. The average percentage drop from the previous week's worth of intervals you want to consider a buy worthy dip. + +The following sections are optional. + +1. AWS credentials: + 1. AWS API keys + 2. SNS topic ARN (us-east-1 only for now) + +These settings should be in a configuration file named `config.json` and placed in `./config`. +Additionally, you can override the volume mount to a new path if you prefer. +The file should look like this: + +```json +{ + "bot": { + "currency": "ETH", + "buy_amount": 75.00, + "dip_percentage": 10 + }, + "coinbase": { + "api_key": "YOUR_API_KEY", + "api_secret": "YOUR_API_SECRET", + "passphrase": "YOUR_API_PASSPHRASE" + }, + "aws": { + "access_key": "YOUR_API_KEY", + "secret_access_key": "YOUR_API_SECRET", + "sns_arn": "arn:aws:sns:us-east-1:012345678901:dip_alerts" + } +} +``` + +Running outside of Docker +------------------------- +You can run the bot outside of Docker pretty easily. + +```bash +python SourceCode/cryptodip-bot.py -c /path/to/config.json +``` + +Logs +---- +The bot will log activity to stdout, so you can review it with `docker logs` + +Donations +--------- +Any and all donations are greatly appreciated. +I have GitHub Sponsors configured however I happily prefer cryptocurrency: + +ETH/ERC20s: ahrenstein.eth (0x288f3d3df1c719176f0f6e5549c2a3928d27d1c1) +BTC: 3HrVPPwTmPG8LKBt84jbQrVjeqDbM1KyEb diff --git a/SourceCode/bot_alerts.py b/SourceCode/bot_alerts.py new file mode 100644 index 0000000..f0a5095 --- /dev/null +++ b/SourceCode/bot_alerts.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Alerting Functions""" +# +# Python Script:: bot_alerts.py +# +# Linter:: pylint +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE +# + + +import json +import boto3 + + +def get_aws_creds_from_file(config_file: str) -> [str, str, str]: + """Open a JSON file and get AWS credentials out of it + Args: + config_file: Path to the JSON file containing credentials + + Returns: + aws_access_key: The AWS access key your bot will use + aws_secret_key: The AWS secret access key + sns_topic_arn: The SNS topic ARN to publish to + """ + with open(config_file) as creds_file: + data = json.load(creds_file) + aws_access_key = data['aws']['access_key'] + aws_secret_key = data['aws']['secret_access_key'] + sns_topic_arn = data['aws']['sns_arn'] + return aws_access_key, aws_secret_key, sns_topic_arn + + +def post_to_sns(aws_access_key: str, aws_secret_key: str, sns_topic_arn: str, + message_subject: str, message_body: str): + """Read all current price records in the database + + Args: + aws_access_key: The AWS access key your bot will use + aws_secret_key: The AWS secret access key + sns_topic_arn: The SNS topic ARN to publish to + message_subject: A message subject to post to SNS + message_body: A message body to post to SNS + """ + sns = boto3.client('sns', region_name="us-east-1", + aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) + sns.publish(TopicArn=sns_topic_arn, Subject=message_subject, Message=message_body) diff --git a/SourceCode/bot_math.py b/SourceCode/bot_math.py new file mode 100644 index 0000000..a21d9be --- /dev/null +++ b/SourceCode/bot_math.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Simple math functions""" +# +# Python Script:: bot_math.py +# +# Linter:: pylint +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE +# + + +def get_average(list_of_numbers: list) -> float: + """Read all current price records in the database + + Args: + list_of_numbers: A list of floats + + Returns: + list_average: A float containing the list average + """ + sum_of_numbers = 0 + for number in list_of_numbers: + sum_of_numbers = sum_of_numbers + number + list_average = sum_of_numbers / len(list_of_numbers) + return round(list_average, 2) + + +def dip_percent_value(price: float, percent: float) -> float: + """Return the value of the current price if it dips a certain percent + + Args: + price: The price to check a dip percentage against + percent: the dip percentage we care about + + Returns: + dip_price: A float containing the price if we hit our dip target + """ + dip_price = price * (1 - percent / 100) + return round(dip_price, 2) diff --git a/SourceCode/coinbase_pro.py b/SourceCode/coinbase_pro.py new file mode 100644 index 0000000..87a9316 --- /dev/null +++ b/SourceCode/coinbase_pro.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Functions to use with Coinbase Pro""" +# +# Python Script:: coinbase_pro.py +# +# Linter:: pylint +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE +# + +import base64 +import json +import time +import hmac +import hashlib +import requests +from requests.auth import AuthBase + + +# Create custom authentication for CoinbasePro +# as per https://docs.pro.coinbase.com/?python#creating-a-request +class CoinbaseProAuth(AuthBase): + """ + Coinbase Pro provided authentication method with minor fixes + """ + def __init__(self, api_key, secret_key, passphrase): + self.api_key = api_key + self.secret_key = secret_key + self.passphrase = passphrase + + def __call__(self, request): + timestamp = str(time.time()) + try: + message = timestamp + request.method + request.path_url + (request.body or b'').decode() + except: + message = timestamp + request.method + request.path_url + (request.body or b'') + hmac_key = base64.b64decode(self.secret_key) + signature = hmac.new(hmac_key, message.encode(), hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()).decode() + + request.headers.update({ + 'CB-ACCESS-SIGN': signature_b64, + 'CB-ACCESS-TIMESTAMP': timestamp, + 'CB-ACCESS-KEY': self.api_key, + 'CB-ACCESS-PASSPHRASE': self.passphrase, + 'Content-Type': 'application/json' + }) + return request + + +def get_cbpro_creds_from_file(config_file: str) -> [str, str, str]: + """Open a JSON file and get Coinbase Pro credentials out of it + Args: + config_file: Path to the JSON file containing credentials and config options + + Returns: + cbpro_api_key: An API key for Coinbase Pro + cbpro_api_secret: An API secret for Coinbase Pro + cbpro_api_passphrase: An API passphrase for Coinbase Pro + """ + with open(config_file) as creds_file: + data = json.load(creds_file) + cbpro_api_key = data['coinbase']['api_key'] + cbpro_api_secret = data['coinbase']['api_secret'] + cbpro_api_passphrase = data['coinbase']['passphrase'] + return cbpro_api_key, cbpro_api_secret, cbpro_api_passphrase + + +def get_coin_price(api_url: str, config_file: str, currency: str) -> float: + """ + Get the USD price of a coin from Coinbase Pro + + Args: + api_url: The API URL for Coinbase Pro + config_file: Path to the JSON file containing credentials and config options + currency: The cryptocurrency the bot is monitoring + + Returns: + coin_price: The price the coin currently holds in USD + """ + # Instantiate Coinbase API and query the price + coinbase_creds = get_cbpro_creds_from_file(config_file) + coinbase_auth = CoinbaseProAuth(coinbase_creds[0], coinbase_creds[1], coinbase_creds[2]) + api_query = "products/%s-USD/ticker" % currency + result = requests.get(api_url + api_query, auth=coinbase_auth) + coin_price = float(result.json()['price']) + return coin_price + + +def verify_balance(api_url: str, config_file: str, buy_amount: float) -> bool: + """Check if enough money exists in the account + Args: + api_url: The API URL for Coinbase Pro + config_file: Path to the JSON file containing credentials and config options + buy_amount: The amount of $USD the bot plans to spend + + Returns: + all_clear: A bool that returns true if there is enough money to transact + """ + # Instantiate Coinbase API and query the price + coinbase_creds = get_cbpro_creds_from_file(config_file) + coinbase_auth = CoinbaseProAuth(coinbase_creds[0], coinbase_creds[1], coinbase_creds[2]) + api_query = "accounts" + result = requests.get(api_url + api_query, auth=coinbase_auth).json() + try: + for account in result: + if account['currency'] == "USD": + if float(account['balance']) >= buy_amount: + return True + except Exception as err: + print("ERROR: Unable to current balance!") + print(err) + return False + # Return false by default + return False + + +def buy_currency(api_url: str, config_file: str, currency: str, buy_amount: float) -> bool: + """ + Conduct a trade on Coinbase Pro to trade a currency with USD + + Args: + api_url: The API URL for Coinbase Pro + config_file: Path to the JSON file containing credentials and config options + currency: The cryptocurrency the bot is monitoring + buy_amount: The amount of $USD the bot plans to spend + + Returns: + trade_success: A bool that is true if the trade succeeded + """ + coinbase_creds = get_cbpro_creds_from_file(config_file) + # Instantiate Coinbase API and query the price + coinbase_auth = CoinbaseProAuth(coinbase_creds[0], coinbase_creds[1], coinbase_creds[2]) + buy_query = 'orders' + order_config = json.dumps({'type': 'market', 'funds': buy_amount, + 'side': 'buy', 'product_id': '%s-USD' % currency}) + buy_result = requests.post(api_url + buy_query, data=order_config, auth=coinbase_auth).json() + if 'message' in buy_result: + print("LOG: Buy order failed.") + print("LOG: Reason: %s" % buy_result['message']) + return False + else: + print("LOG: Buy order succeeded.") + print("LOG: Buy Results: %s" % json.dumps(buy_result, indent=2)) + return True diff --git a/SourceCode/cryptodip_bot.py b/SourceCode/cryptodip_bot.py new file mode 100644 index 0000000..f432cdf --- /dev/null +++ b/SourceCode/cryptodip_bot.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""A bot that attempts to buy cryptocurrency on the dip.""" +# +# Python Script:: cryptodip_bot.py +# +# Linter:: pylint +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE +# + +from itertools import count +import argparse +import datetime +import json +import time +import bot_alerts +import bot_math +import coinbase_pro +import mongo + +# Constants that might be useful to adjust for debugging purposes +CYCLE_INTERVAL_MINUTES = 60 + + +def read_bot_config(config_file: str) -> [str, float, int, bool]: + """Open a JSON file and get the bot configuration + Args: + config_file: Path to the JSON file containing credentials and config options + + Returns: + crypto_currency: The cryptocurrency that will be monitored + buy_amount: The price in $USD that will be purchased when a dip is detected + dip_percentage: The percentage of the average price drop that means a dip occurred + aws_loaded: A bool to determine if AWS configuration options exist + """ + with open(config_file) as creds_file: + data = json.load(creds_file) + crypto_currency = data['bot']['currency'] + buy_amount = data['bot']['buy_amount'] + dip_percentage = data['bot']['dip_percentage'] + aws_loaded = bool('aws' in data) + return crypto_currency, buy_amount, dip_percentage, aws_loaded + + +def main(config_file: str, debug_mode: bool): + """ + The main function that triggers and runs the bot functions + + Args: + config_file: Path to the JSON file containing credentials and config options + debug_mode: Use Sandbox APIs instead of production + """ + # Load the configuration file + config_params = read_bot_config(config_file) + if config_params[3]: + aws_config = bot_alerts.get_aws_creds_from_file(config_file) + message = "%s-Bot has been started" % config_params[0] + bot_alerts.post_to_sns(aws_config[0], aws_config[1], aws_config[2], message, message) + # Set API URLs + coinbase_pro_api_url = "" + mongo_db_connection = "" + if debug_mode: + coinbase_pro_api_url = "https://api-public.sandbox.pro.coinbase.com/" + mongo_db_connection = "mongodb://bots:buythedip@bots-db:27017/" + else: + coinbase_pro_api_url = "https://api.pro.coinbase.com/" + mongo_db_connection = "mongodb://bots:buythedipbots-db:27017/" + print("LOG: Starting bot...") + print("LOG: Monitoring %s to buy $%s worth when a %s%% dip occurs." + % (config_params[0], config_params[1], config_params[2])) + # Execute the bot every 10 seconds + for cycle in count(): + now = datetime.datetime.now().strftime("%m/%d/%Y-%H:%M:%S") + print("LOG: Cycle %s: %s" % (cycle, now)) + # Verify that there is enough money to transact, otherwise don't bother + if not coinbase_pro.verify_balance(coinbase_pro_api_url, config_file, config_params[1]): + message = "LOG: Not enough account balance" \ + " to buy $%s worth of %s" % (config_params[1], config_params[0]) + subject = "%s-Bot Funding Issue" % config_params[0] + if config_params[3]: + bot_alerts.post_to_sns(aws_config[0], aws_config[1], aws_config[2], + subject, message) + print("LOG: %s" % message) + # Sleep for the specified cycle interval then end the cycle + time.sleep(CYCLE_INTERVAL_MINUTES * 60) + continue + coin_current_price = coinbase_pro.get_coin_price\ + (coinbase_pro_api_url, config_file, config_params[0]) + # Add the current price to the price database + mongo.add_price(mongo_db_connection, config_params[0], coin_current_price) + # Check if the a week has passed since the last dip buy + clear_to_proceed = mongo.check_last_buy_date(mongo_db_connection, config_params[0]) + if clear_to_proceed is True: + print("LOG: Last buy date over a week ago. Checking if a dip is occurring.") + average_price = mongo.average_pricing(mongo_db_connection, config_params[0]) + dip_price = bot_math.dip_percent_value(average_price, config_params[2]) + print("LOG: A %s%% dip at the average price of %s would be %s" + %(config_params[2], average_price, dip_price)) + if coin_current_price <= dip_price: + print("LOG: The current price of %s is <= %s. We are in a dip!" + % (coin_current_price, dip_price)) + did_buy = coinbase_pro.buy_currency(coinbase_pro_api_url, + config_file, config_params[0], config_params[1]) + message = "Buy success status is %s for %s worth of %s"\ + % (did_buy, config_params[1], config_params[0]) + subject = "%s-Bot Buy Status Alert" % config_params[0] + print("LOG: %s" % message) + if config_params[3]: + bot_alerts.post_to_sns(aws_config[0], aws_config[1], aws_config[2], + subject, message) + else: + print("LOG: The current price of %s is > %s. We are not in a dip!" + % (coin_current_price, dip_price)) + else: + print("LOG: Last buy date under a week ago. No buys will be attempted.") + + # Run a price history cleanup daily otherwise sleep the interval + if (cycle * CYCLE_INTERVAL_MINUTES) % 1440 == 0: + print("LOG: Cleaning up price history older than 30 days.") + mongo.cleanup_old_records(mongo_db_connection, config_params[0]) + else: + # Sleep for the specified cycle interval + time.sleep(CYCLE_INTERVAL_MINUTES * 60) + + +if __name__ == '__main__': + # This function parses and return arguments passed in + # Assign description to the help doc + PARSER = argparse.ArgumentParser( + description='A bot that attempts to buy' + ' cryptocurrency on the dip.') + # Add arguments + PARSER.add_argument( + '-c', '--configFile', type=str, help="Path to config.json file", required=True + ) + PARSER.add_argument( + '-d', '--debug', help="Use sandbox APIs", required=False, action='store_true' + ) + # Array for all arguments passed to script + ARGS = PARSER.parse_args() + ARG_CONFIG = ARGS.configFile + ARG_DEBUG = ARGS.debug + main(ARG_CONFIG, ARG_DEBUG) diff --git a/SourceCode/mongo.py b/SourceCode/mongo.py new file mode 100644 index 0000000..9532141 --- /dev/null +++ b/SourceCode/mongo.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Functions to use with MongoDB""" +# +# Python Script:: mongo.py +# +# Linter:: pylint +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE +# + +import datetime +import pymongo +import bot_math + +# Constants that might be useful to adjust for debugging purposes +AVERAGE_PERIOD_DAYS = 7 +PURGE_OLDER_THAN_DAYS = 30 + + +def add_price(db_server: str, currency: str, current_price: float): + """Add a current price record to the database + + Args: + db_server: The MongoDB server to connect to + currency: The cryptocurrency the bot is monitoring + current_price: The current price of the currency + """ + timestamp = datetime.datetime.utcnow() + # Create a Mongo client to connect to + mongo_client = pymongo.MongoClient(db_server) + bot_db = mongo_client["%s-bot" % currency] + prices_collection = bot_db["prices"] + record = {"time": timestamp, "price": current_price} + try: + prices_collection.insert_one(record) + except Exception as err: + print("Error creating price record: %s" % err) + + +def read_all_prices(db_server: str, currency: str): + """Read all current price records in the database + + Args: + db_server: The MongoDB server to connect to + currency: The cryptocurrency the bot is monitoring + """ + # Create a Mongo client to connect to + mongo_client = pymongo.MongoClient(db_server) + bot_db = mongo_client["%s-bot" % currency] + prices_collection = bot_db["prices"] + records = prices_collection.find() + for record in records: + print(record) + + +def average_pricing(db_server: str, currency: str) -> float: + """Check the last week of prices and return the average + + Args: + db_server: The MongoDB server to connect to + currency: The cryptocurrency the bot is monitoring + + Returns: + average_price: The average price of the last week + """ + price_history = [] + # Create a Mongo client to connect to + mongo_client = pymongo.MongoClient(db_server) + bot_db = mongo_client["%s-bot" % currency] + prices_collection = bot_db["prices"] + records = prices_collection.find({}) + for record in records: + record_age = datetime.datetime.utcnow() - record['time'] + if record_age.days <= AVERAGE_PERIOD_DAYS: + price_history.append(record['price']) + average_price = bot_math.get_average(price_history) + return average_price + + +def cleanup_old_records(db_server: str, currency: str): + """Remove all price history older than 30 days + + Args: + db_server: The MongoDB server to connect to + currency: The cryptocurrency the bot is monitoring + """ + # Create a Mongo client to connect to + mongo_client = pymongo.MongoClient(db_server) + bot_db = mongo_client["%s-bot" % currency] + prices_collection = bot_db["prices"] + records = prices_collection.find() + for record in records: + record_age = datetime.datetime.utcnow() - record['time'] + if record_age.days >= PURGE_OLDER_THAN_DAYS: + prices_collection.delete_one({"_id": record['_id']}) + + +def set_last_buy_date(db_server: str, currency: str): + """Sets the date the last time the currency was bought + + Args: + db_server: The MongoDB server to connect to + currency: The cryptocurrency the bot is monitoring + """ + timestamp = datetime.datetime.utcnow() + # Create a Mongo client to connect to + mongo_client = pymongo.MongoClient(db_server) + bot_db = mongo_client["%s-bot" % currency] + buy_date = bot_db["buy-date"] + try: + buy_date.find_one_and_update({"_id": 1}, + {"$set": {"time": timestamp}}, upsert=True) + except Exception as err: + print("Error updating buy date record: %s" % err) + + +def check_last_buy_date(db_server: str, currency: str) -> bool: + """Get the date of the last time the currency was bought + and returns true if it was at least a week ago + + Args: + db_server: The MongoDB server to connect to + currency: The cryptocurrency the bot is monitoring + + Returns: + clear_to_buy: A bool that is true if we are clear to buy + """ + # Create a Mongo client to connect to + mongo_client = pymongo.MongoClient(db_server) + bot_db = mongo_client["%s-bot" % currency] + buy_date = bot_db["buy-date"] + # Create an initial record if the record doesn't exist yet + if buy_date.find({'_id': 1}).count() == 0: + print("Initializing new last buy date") + timestamp = datetime.datetime.utcnow() + buy_date.find_one_and_update({"_id": 1}, + {"$set": {"time": timestamp}}, upsert=True) + return False + try: + last_buy_date = buy_date.find_one({"_id": 1})['time'] + except Exception as err: + print("Error getting buy date record: %s" % err) + return False + time_difference = datetime.datetime.utcnow() - last_buy_date + return time_difference.days >= 7 diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..9cfff95 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,29 @@ +Crypto Dip Buying Bot: Testing +============================== +I test this bot against Coinbase Pro sandbox first then against my live Coinbase Pro account. + +Testing Requirements +-------------------- +All modules tested must follow these testing rules: + +1. All modules must be tested against an AWS account with all optional variables tested. +2. Changes to modules should be tested to avoid breaking existing infrastructure. +3. Code should pass `pre-commit` checks. + +pre-commit +---------- +This repo uses Yelp's [pre-commit](https://pre-commit.com/) to manage some pre-commit hooks automatically. +In order to use the hooks, make sure you have `pre-commit`, and `pylint` in your `$PATH`. +Once in your path you should run `pre-commit install` in order to configure it. If you push commits that fail pre-commit, your PR will +not be merged. + +poetry +------ +This project uses [poetry](https://python-poetry.org/) for Python requirements +both for development and building the Docker container. + +Python tests +------------ +Coming soon ;) + +Return to [README](README.md) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0fa1d10 --- /dev/null +++ b/TODO.md @@ -0,0 +1,27 @@ +Crypto Dip Buying Bot: To Do List +================================= +The versions below are future versions that will be released once those features have been completed. + +Version 0.2.0 +------------- + +1. Make cool down period and check period variables + 1. Assume defaults if not specified + +Version 0.3.0 +------------- + +1. Exception catching around DB functions +2. Cleaner Python code (The big O on this is probably shit) + +Version 0.5.0 +------------- + +1. Find a more secure way to store credentials than an on disk JSON file. + +Version 1.0.0 +------------- + +1. Python code is tested using pytest + +Return to [README](README.md) diff --git a/config/.gitignore b/config/.gitignore new file mode 100644 index 0000000..f45ab25 --- /dev/null +++ b/config/.gitignore @@ -0,0 +1,2 @@ +*.json +db-data/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e9c451d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +# +# Copyright 2021, Matthew Ahrenstein, All Rights Reserved. +# +# Maintainers: +# - Matthew Ahrenstein: matt@ahrenstein.com +# +# See LICENSE + +version: '2' +services: + bots-db: + container_name: bots-db + image: mongo:3.6 + volumes: + - ./config/db-data:/data/db + environment: + MONGO_INITDB_ROOT_USERNAME: bots + MONGO_INITDB_ROOT_PASSWORD: buythedip + expose: + - 27017 + restart: always + eth-dips: + links: + - bots-db + container_name: eth-dips + image: ahrenstein/cryptodip-bot:latest + volumes: + - ./config:/config + restart: always + btc-dips: + links: + - bots-db + container_name: btc-dips + image: ahrenstein/cryptodip-bot:latest + volumes: + - ./config:/config + command: [ "python", "-u", "/app/cryptodip_bot.py", "-c", "/config/btc-config.json", "-d"] + restart: always diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..64d5b9b --- /dev/null +++ b/poetry.lock @@ -0,0 +1,390 @@ +[[package]] +name = "astroid" +version = "2.5.2" +description = "An abstract syntax tree for Python with inference support." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +wrapt = ">=1.11,<1.13" + +[[package]] +name = "boto3" +version = "1.17.49" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +botocore = ">=1.20.49,<1.21.0" +jmespath = ">=0.7.1,<1.0.0" +s3transfer = ">=0.3.0,<0.4.0" + +[[package]] +name = "botocore" +version = "1.20.49" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +jmespath = ">=0.7.1,<1.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.10.8)"] + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "isort" +version = "5.8.0" +description = "A Python utility / library to sort Python imports." +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "jmespath" +version = "0.10.0" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "lazy-object-proxy" +version = "1.6.0" +description = "A fast and thorough lazy object proxy." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pylint" +version = "2.7.4" +description = "python code static checker" +category = "main" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +astroid = ">=2.5.2,<2.7" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[package.extras] +docs = ["sphinx (==3.5.1)", "python-docs-theme (==2020.12)"] + +[[package]] +name = "pymongo" +version = "3.11.3" +description = "Python driver for MongoDB " +category = "main" +optional = false +python-versions = "*" + +[package.extras] +aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["pymongocrypt (<2.0.0)"] +gssapi = ["pykerberos"] +ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +srv = ["dnspython (>=1.16.0,<1.17.0)"] +tls = ["ipaddress"] +zstd = ["zstandard"] + +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "s3transfer" +version = "0.3.6" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.26.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotlipy (>=0.6.0)"] + +[[package]] +name = "wrapt" +version = "1.12.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "6d00e7cc397ec0c2fdae35aeb61301d984a48e6c7158b8fbdae90bbb974aa29c" + +[metadata.files] +astroid = [ + {file = "astroid-2.5.2-py3-none-any.whl", hash = "sha256:cd80bf957c49765dce6d92c43163ff9d2abc43132ce64d4b1b47717c6d2522df"}, + {file = "astroid-2.5.2.tar.gz", hash = "sha256:6b0ed1af831570e500e2437625979eaa3b36011f66ddfc4ce930128610258ca9"}, +] +boto3 = [ + {file = "boto3-1.17.49-py2.py3-none-any.whl", hash = "sha256:d5ef160442925f5944e4cde88589f0f195f6c284f05613114fc6bbc35e342fa7"}, + {file = "boto3-1.17.49.tar.gz", hash = "sha256:a482135c30fa07eaf4370314dd0fb49117222a266d0423b2075aed3835ed1f04"}, +] +botocore = [ + {file = "botocore-1.20.49-py2.py3-none-any.whl", hash = "sha256:6a672ba41dd00e5c1c1824ca8143d180d88de8736d78c0b1f96b8d3cb0466561"}, + {file = "botocore-1.20.49.tar.gz", hash = "sha256:f7f103fa0651c69dd360c7d0ecd874854303de5cc0869e0cbc2818a52baacc69"}, +] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +isort = [ + {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, + {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, +] +jmespath = [ + {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, + {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +pylint = [ + {file = "pylint-2.7.4-py3-none-any.whl", hash = "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a"}, + {file = "pylint-2.7.4.tar.gz", hash = "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"}, +] +pymongo = [ + {file = "pymongo-3.11.3-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:4d959e929cec805c2bf391418b1121590b4e7d5cb00af7b1ba521443d45a0918"}, + {file = "pymongo-3.11.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9fbffc5bad4df99a509783cbd449ed0d24fcd5a450c28e7756c8f20eda3d2aa5"}, + {file = "pymongo-3.11.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:bd351ceb2decd23d523fc50bad631ee9ae6e97e7cdc355ce5600fe310484f96e"}, + {file = "pymongo-3.11.3-cp27-cp27m-win32.whl", hash = "sha256:7d2ae2f7c50adec20fde46a73465de31a6a6fbb4903240f8b7304549752ca7a1"}, + {file = "pymongo-3.11.3-cp27-cp27m-win_amd64.whl", hash = "sha256:b1aa62903a2c5768b0001632efdea2e8da6c80abdd520c2e8a16001cc9affb23"}, + {file = "pymongo-3.11.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:180511abfef70feb022360b35f4863dd68e08334197089201d5c52208de9ca2e"}, + {file = "pymongo-3.11.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:42f9ec9d77358f557fe17cc15e796c4d4d492ede1a30cba3664822cae66e97c5"}, + {file = "pymongo-3.11.3-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:3dbc67754882d740f17809342892f0b24398770bd99d48c5cb5ba89f5f5dee4e"}, + {file = "pymongo-3.11.3-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:733e1cfffc4cd99848230e2999c8a86e284c6af6746482f8ad2ad554dce14e39"}, + {file = "pymongo-3.11.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:622a5157ffcd793d305387c1c9fb94185f496c8c9fd66dafb59de0807bc14ad7"}, + {file = "pymongo-3.11.3-cp34-cp34m-win32.whl", hash = "sha256:2aeb108da1ed8e066800fb447ba5ae89d560e6773d228398a87825ac3630452d"}, + {file = "pymongo-3.11.3-cp34-cp34m-win_amd64.whl", hash = "sha256:7c77801620e5e75fb9c7abae235d3cc45d212a67efa98f4972eef63e736a8daa"}, + {file = "pymongo-3.11.3-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:29390c39ca873737689a0749c9c3257aad96b323439b11279fbc0ba8626ec9c5"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a8b02e0119d6ee381a265d8d2450a38096f82916d895fed2dfd81d4c7a54d6e4"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28633868be21a187702a8613913e13d1987d831529358c29fc6f6670413df040"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:685b884fa41bd2913fd20af85866c4ff886b7cbb7e4833b918996aa5d45a04be"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:7cd42c66d49ffb68dea065e1c8a4323e7ceab386e660fee9863d4fa227302ba9"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:950710f7370613a6bfa2ccd842b488c5b8072e83fb6b7d45d99110bf44651d06"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:c7fd18d4b7939408df9315fedbdb05e179760960a92b3752498e2fcd03f24c3d"}, + {file = "pymongo-3.11.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:cc359e408712faf9ea775f4c0ec8f2bfc843afe47747a657808d9595edd34d71"}, + {file = "pymongo-3.11.3-cp35-cp35m-win32.whl", hash = "sha256:7814b2cf23aad23464859973c5cd2066ca2fd99e0b934acefbb0b728ac2525bf"}, + {file = "pymongo-3.11.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e1414599a97554d451e441afb362dbee1505e4550852c0068370d843757a3fe2"}, + {file = "pymongo-3.11.3-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:0384d76b409278ddb34ac19cdc4664511685959bf719adbdc051875ded4689aa"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:22ee2c94fee1e391735be63aa1c9af4c69fdcb325ae9e5e4ddff770248ef60a6"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:db6fd53ef5f1914ad801830406440c3bfb701e38a607eda47c38adba267ba300"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:66b688fc139c6742057795510e3b12c4acbf90d11af1eff9689a41d9c84478d6"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:6a5834e392c97f19f36670e34bf9d346d733ad89ee0689a6419dd737dfa4308a"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:87981008d565f647142869d99915cc4760b7725858da3d39ecb2a606e23f36fd"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:413b18ac2222f5d961eb8d1c8dcca6c6ca176c8613636d8c13aa23abae7f7a21"}, + {file = "pymongo-3.11.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:610d5cbbfd026e2f6d15665af51e048e49b68363fedece2ed318cc8fe080dd94"}, + {file = "pymongo-3.11.3-cp36-cp36m-win32.whl", hash = "sha256:3873866534b6527e6863e742eb23ea2a539e3c7ee00ad3f9bec9da27dbaaff6f"}, + {file = "pymongo-3.11.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b17e627844d86031c77147c40bf992a6e1114025a460874deeda6500d0f34862"}, + {file = "pymongo-3.11.3-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:05e2bda928a3a6bc6ddff9e5a8579d41928b75d7417b18f9a67c82bb52150ac6"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:19d52c60dc37520385f538d6d1a4c40bc398e0885f4ed6a36ce10b631dab2852"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2163d736d6f62b20753be5da3dc07a188420b355f057fcbb3075b05ee6227b2f"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b4535d98df83abebb572035754fb3d4ad09ce7449375fa09fa9ede2dbc87b62b"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:cd8fc35d4c0c717cc29b0cb894871555cb7137a081e179877ecc537e2607f0b9"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:92e2376ce3ca0e3e443b3c5c2bb5d584c7e59221edfb0035313c6306049ba55a"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:4ca92e15fcf02e02e7c24b448a16599b98c9d0e6a46cd85cc50804450ebf7245"}, + {file = "pymongo-3.11.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5a03ae5ac85b04b2034a0689add9ff597b16d5e24066a87f6ab0e9fa67049156"}, + {file = "pymongo-3.11.3-cp37-cp37m-win32.whl", hash = "sha256:bc2eb67387b8376120a2be6cba9d23f9d6a6c3828e00fb0a64c55ad7b54116d1"}, + {file = "pymongo-3.11.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5e1341276ce8b7752db9aeac6bbb0cbe82a3f6a6186866bf6b4906d8d328d50b"}, + {file = "pymongo-3.11.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4ac387ac1be71b798d1c372a924f9c30352f30e684e06f086091297352698ac0"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:728313cc0d59d1a1a004f675607dcf5c711ced3f55e75d82b3f264fd758869f3"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:daa44cefde19978af57ac1d50413cd86ebf2b497328e7a27832f5824bda47439"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:322f6cc7bf23a264151ebc5229a92600c4b55ac83c83c91c9bab1ec92c888a8d"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6043d251fac27ca04ff22ed8deb5ff7a43dc18e8a4a15b4c442d2a20fa313162"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:66573c8c7808cce4f3b56c23cb7cad6c3d7f4c464b9016d35f5344ad743896d7"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bf70097bd497089f1baabf9cbb3ec4f69c022dc7a70c41ba9c238fa4d0fff7ab"}, + {file = "pymongo-3.11.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:f23abcf6eca5859a2982beadfb5111f8c5e76e30ff99aaee3c1c327f814f9f10"}, + {file = "pymongo-3.11.3-cp38-cp38-win32.whl", hash = "sha256:1d559a76ae87143ad96c2ecd6fdd38e691721e175df7ced3fcdc681b4638bca1"}, + {file = "pymongo-3.11.3-cp38-cp38-win_amd64.whl", hash = "sha256:152e4ac3158b776135d8fce28d2ac06e682b885fcbe86690d66465f262ab244e"}, + {file = "pymongo-3.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34c15f5798f23488e509eae82fbf749c3d17db74379a88c07c869ece1aa806b9"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:210ec4a058480b9c3869082e52b66d80c4a48eda9682d7a569a1a5a48100ea54"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b44fa04720bbfd617b6aef036989c8c30435f11450c0a59136291d7b41ed647f"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b32e4eed2ef19a20dfb57698497a9bc54e74efb2e260c003e9056c145f130dc7"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:5091aacbdb667b418b751157f48f6daa17142c4f9063d58e5a64c90b2afbdf9a"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:bb6a5777bf558f444cd4883d617546182cfeff8f2d4acd885253f11a16740534"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:980527f4ccc6644855bb68056fe7835da6d06d37776a52df5bcc1882df57c3db"}, + {file = "pymongo-3.11.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:65b67637f0a25ac9d25efb13c1578eb065870220ffa82f132c5b2d8e43ac39c3"}, + {file = "pymongo-3.11.3-cp39-cp39-win32.whl", hash = "sha256:f6748c447feeadda059719ef5ab1fb9d84bd370e205b20049a0e8b45ef4ad593"}, + {file = "pymongo-3.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:ee42a8f850143ae7c67ea09a183a6a4ad8d053e1dbd9a1134e21a7b5c1bc6c73"}, + {file = "pymongo-3.11.3-py2.7-macosx-10.14-intel.egg", hash = "sha256:7edff02e44dd0badd749d7342e40705a398d98c5d8f7570f57cff9568c2351fa"}, + {file = "pymongo-3.11.3.tar.gz", hash = "sha256:db5098587f58fbf8582d9bda2462762b367207246d3e19623782fb449c3c5fcc"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +s3transfer = [ + {file = "s3transfer-0.3.6-py2.py3-none-any.whl", hash = "sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2"}, + {file = "s3transfer-0.3.6.tar.gz", hash = "sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +urllib3 = [ + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, +] +wrapt = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..88d62ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "cryptodip-bot" +version = "0.1.0" +description = "A bot that attempts to buy cryptocurrency on the dip." +authors = ["Matthew Ahrenstein "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.8" +requests = "^2.25.1" +pylint = "^2.7.4" +pymongo = "^3.11.3" +urllib3 = "^1.26.4" +boto3 = "^1.17.49" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"