DEV Community

Cover image for Building a Crypto Trading Bot in Python 101: Automatic Deposits on Coinbase Pro
Taylor Facen
Taylor Facen

Posted on

Building a Crypto Trading Bot in Python 101: Automatic Deposits on Coinbase Pro

Coinbase Pro is a pretty easy to use crypto exchange. Although it doesn’t allow trading of the entire crypto universe, it does allow users to deposit and trade with fiat currencies. One feature that’s missing, but pretty easy to implement, is automatic deposits.

This tutorial will walk you through using Python to initiate deposits on a periodic basis. I know what you're thinking, "Making automatic deposits does not constitute having a trading bot". Future tutorials will build on this one to complete out all of the bot features. Stay tuned!

Want to skip to the end? Here's the repo with this tutorial's code.

Technologies used

  • Python - Programming Language
  • Logger - Python package for logging events
  • AWS S3 - Storage for logging events
  • Heroku - Deployment
  • Coinbase Pro - Crypto Exchange

Contents

Coinbase Pro Set Up

To get started, make sure you have a production account with Coinbase Pro and an account on the Coinbase Pro Sandbox (this is used for testing). For both accounts, create API credentials

  1. Profile in upper right corner -> API -> New API Key
  2. Permissions - Later on, I'll show you how to place trades, so for now you can select all three (view, transfer, trade)
  3. Passphrase - leave as default
  4. IP Whitelist - leave as default
  5. Two Factor Authentication Code - depends on how you set up your account

API key set up screenshot

When you're done, you should have the following for both your production and sandbox accounts:

  • Passphrase
  • Secret
  • Key

Flask App

First create a folder for this project and then initiate and activate a virtual environment

mkdir crypto_trading_bot
cd crypto_trading_bot
virtualenv env
source env/bin/activate
Enter fullscreen mode Exit fullscreen mode

Let's go ahead and install Flask and create the requirements file.
Note: Make sure your virtual environment is activated. If not, all of the packages installed on your computer will be added to the requirements file.

pip install Flask
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Next, create a file with the following
Filename: app.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/", methods = ['GET'] )
def func():
    return jsonify({"message": "Hello World"})

if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

This code create a basic Flask app with a single endpoint. Let's test it out. In your terminal write

python app.py

. This runs the Flask app on your local computer. In your terminal, you should see a line that says something like "Running on http://###.#.#.#:###/". If you go to this URL in your browser, you should see "message: Hello World"

Alright, now that the app is set up, let's deploy it to Heroku.

Heroku Set Up

Usually, tutorials walk you through the entire process before showing you how to deploy it. I'm going to go through the deployment step now because Heroku allows for automatic deployments when code is pushed to a branch. Also, from this point on, we'll need to utilize secure information like secrets and passphrases. Using Heroku's environment configuration is way easier than adding all of the keys to my computer's bash_profile, and doing so makes sure you don't mix keys in different projects.

If you haven't already, create an account on Heroku. Then, install the Heroku CLI to your computer and go through the set up process. Afterwords, create an app and head over to the Deploy page to connect it to the repository where you're pushing your code.

  • Deployment Method - Github (feel free to use Heroku Git if you want)
  • Automatic deploys - Select the branch you want to automatically deploy and then click "Enable Automatic Deploys"

Before we can check out our app, we have to serve it. We'll do so using Gunicorn. Run the following in your terminal (make sure your virtualenv is still running).

pip install gunicorn
pip freeze > requirements.txt 
Enter fullscreen mode Exit fullscreen mode

Then, add the following file.
Filename: Procfile
(warning, there's no extension to this file name)

web: gunicorn app:app
Enter fullscreen mode Exit fullscreen mode

Now commit your changes, wait about a minute, and then head over to your site! You can find your domain on the "Settings" page in the "Domain and certificates" section.

Now that that's set up, let's connect the app to Coinbase Pro.

Connecting to Coinbase Pro

Let's create an environment file so that we can deploy the app locally for testing.

Filename: .env

CB_KEY=ENTER_YOUR_SANDBOX_KEY_HERE
CB_PASSPHRASE=ENTER_YOUR_SANDBOX_PASSPHRASE_HERE
CB_SECRET=ENTER_YOUR_SANDBOX_SECRET_HERE
CB_URL=http://api-public.sandbox.pro.coinbase.com
Enter fullscreen mode Exit fullscreen mode

If you're uploading your code to a public repo, make sure to add .env to your .gitignore file.

Next, let's install the Coinbase Pro Python package.

pip install cbpro
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Because I already know that I'll be adding multiple functions (modules) that will need to connect to my Coinbase Pro account (making deposits, placing trades, viewing balances, etc.), I'm going to create a decorator function that will pass my Coinbase authenticated client to any necessary functions. Don't worry, I actually JUST learned about decorator functions. So if this part doesn't make sense to you, don't worry. Decorators don't make sense to most of us.

Filename: cbpro_client.py

import cbpro

from config import CB_CREDENTIALS

def get_client(credentials):
    """Returns the cbpro AuthenticatedClient using the credentials from the parameters dict"""

    cbpro_client = cbpro.AuthenticatedClient(credentials['KEY'], credentials['SECRET'], credentials['PASSPHRASE'], api_url=credentials['URL'])
    return cbpro_client

def cbpro_client(func):
    def function_wrapper(*args, **kwargs):
        cbpro_client = get_client(CB_CREDENTIALS)
        resp = func(cbpro_client = cbpro_client, *args, **kwargs)

        return resp

    return function_wrapper
Enter fullscreen mode Exit fullscreen mode

In the same folder, create a config file that will read your credentials from the .env file (when using Heroku locally) or your bash_profile (when using python app.py).

Filename: config.py

# Coinbase Credentials
CB_CREDENTIALS = {
    'PASSPHRASE': os.environ['CB_PASSPHRASE'],
    'SECRET': os.environ['CB_SECRET'],
    'KEY': os.environ['CB_KEY'],
    'URL': os.environ['CB_URL']
}
Enter fullscreen mode Exit fullscreen mode

Next up, we're making a deposit!

Making Deposits

Alright, this is the moment we've all been waiting for.

Filename: deposit_funds.py

from cbpro_client import cbpro_client

@cbpro_client
def get_deposit_account(cbpro_client):
    """ Gets ID of account's ACH bank account (assumes there's only one)

    Params:
        - None
    Return:
        - account: dict
        {
            {
            "allow_buy": bool, 
            "allow_deposit": bool, 
            "allow_sell": bool, 
            "allow_withdraw": bool, 
            "cdv_status": str, 
            "created_at": time, 
            "currency": str, 
            "id": str, 
            "limits": dict
            "name": str, 
            "primary_buy": bool, 
            "primary_sell": bool, 
            "resource": str, 
            "resource_path": str, 
            "type": str, 
            "updated_at": time, 
            "verification_method": str, 
            "verified": bool
            }
        }
    """

    bank_accounts = cbpro_client.get_payment_methods()

    for account in bank_accounts:
        # This assumes that there is only one ACH bank account connected
        if account['type'] == 'ach_bank_account':
            return account



@cbpro_client
def deposit_funds(cbpro_client, deposit_amount = 10): # Default deposit amount is $10
    """ Makes deposit into USD Wallet

    Params: 
        - deposit_amonut: int (default 10)
    Return: 
        - deposit response
        {
            'id' : str,
            'amount' : str,
            'currency' : 'USD',
            'payout_at' : str (datetime)
        }
    """

    deposit_account_id = get_deposit_account()['id']

    resp = cbpro_client.deposit(deposit_amount, 'USD', deposit_account_id)

    return resp
Enter fullscreen mode Exit fullscreen mode

Let's unpack. The first function retrieves the bank account id for the account used to make the deposit. This function assumes that there's only one ACH account. So, if you have multiple ach accounts connected to your Coinbase account, feel free to customize how the account is selected. The second function uses the returned bank account to make a deposit.

*Random: I'm still trying to figure out my docstring style. If anyone has any tips or suggestions, feel free to share!

Now, let's add this function to our app so that we can test it out! In your app.py file, add the following to the top

from deposit_funds import deposit_funds
Enter fullscreen mode Exit fullscreen mode

And right above "if name == 'main':", add a new endpoint

@app.route("/deposit", methods = ['GET'])
def deposit_function():
    resp = deposit_funds()
    return jsonify(resp)
Enter fullscreen mode Exit fullscreen mode

Normally, this is where I would say, "To test it out, run the following in your terminal and then head to the deposit endpoint (http://0.0.0.0:####/deposit)"

heroku local
Enter fullscreen mode Exit fullscreen mode

However, this would produce an error because Coinbase doesn't let you deposit funds via API in sandbox mode. So unfortunately, this won't work. However, when you switch over to your real Coinbase credentials, it will be good to go! If you don't believe me (it's okay, we just met), then import the get_deposit_account function instead and use that function for the deposit endpoint. After running your app locally, you should see a JSON object of your deposit account in the browser. Don't forget to switch back to the deposit_funds function.

Now I don't know about you, but I think it would be pretty nice to see the output of each step within the code so I can make sure everything is up and running correctly. Next, we'll add some logging.

Logging

*Feel free to skip this step if logging isn't your flavor

Logging is an AWESOME tool to help you see what's going on in your code (no more print statements!). Because I want to be extra fancy, we'll publish log files to AWS S3 buckets so that we can view them easily.

Here's a tutorial on creating an IAM user on AWS. Also, here's how you create a bucket on AWS S3. Create a bucket and write down the name (e.g. crypto_bot_logs)

Let's create our logger.
First, install boto3, a package to access AWS resources via Python

pip install boto3
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Second, add your IAM credentials to your .env file
Filename: .env

AWS_ACCESS_KEY_ID=ENTER_YOUR_KEY_HERE
AWS_SECRET_ACCESS_KEY=ENTER_YOUR_SECRET_HERE
Enter fullscreen mode Exit fullscreen mode

Thirdly, grab your credentials in your config file by adding the following to the end
Filename: config.py

# AWS Credentials
AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID']
AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY']
Enter fullscreen mode Exit fullscreen mode

Fourthly (is fourthly even a word?), create your logger file
Filename: logger.py

import boto3
from datetime import datetime
import logging

from config import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY

s3_client = boto3.client('s3', 
        aws_access_key_id = AWS_ACCESS_KEY_ID, 
        aws_secret_access_key = AWS_SECRET_ACCESS_KEY,
        region_name = 'us-east-1')

def create_logger(logger_name):
    logger = logging.getLogger(logger_name)
    logger.setLevel(logging.DEBUG)
    # create file handler which logs even debug messages
    fh = logging.FileHandler('{}_{}.log'.format(logger_name, datetime.now().isoformat().split('.')[0]))
    fh.setLevel(logging.DEBUG)
    # create formatter and add it to the handlers
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    fh.setFormatter(formatter)
    # add the handlers to the logger
    logger.addHandler(fh)

    return logger

def upload_log(logger):
    file_name = logger.handlers[0].baseFilename
    directory = datetime.now().date().isoformat()
    key = "{}/{}".format(directory, file_name.split('/')[-1])
    bucket_name = ENTER_YOUR_BUCKET_FILENAME_HERE

    s3_client.upload_file(Filename = file_name, Bucket = bucket_name, Key = key)

def logger(func):
    def function_wrapper(*args, **kwargs):
        function_name = func.__name__
        logger = create_logger(function_name)
        logger.info("Now running - {}".format(function_name))

        resp = func(logger = logger, *args, **kwargs)
        upload_log(logger)

        return resp

    return function_wrapper
Enter fullscreen mode Exit fullscreen mode

Here's what each function does:

  • create_logger - creates a logger with a FileHandler
  • upload_log - Uploads the recently created log file to the designated file
  • logger - This is a decorator function that creates a logger, passes it to the function, and then uploads the log file when the function finishes

Now, we can add some logging statements to our functions. Here's what I added, but feel free to customize based on what you want to see. Here's an awesome tutorial on logging.

Here's how my deposit_funds function looks now. Don't forget to import logger at the top

Filename: deposit_funds.py

@cbpro_client
@logger
def deposit_funds(cbpro_client, logger, deposit_amount = 10): # Default deposit amount is $10
    """ Makes deposit into USD Wallet

    Params: 
        - deposit_amonut: int (default 10)
    Return: 
        - deposit response
        {
            'id' : str,
            'amount' : str,
            'currency' : 'USD',
            'payout_at' : str (datetime)
        }
    """
    logger.info("Getting account ID")
    deposit_account_id = get_deposit_account()['id']
    logger.info("Account ID: {}".format(deposit_account_id))

    resp = cbpro_client.deposit(deposit_amount, 'USD', deposit_account_id)
    if 'message' in resp.keys():
        logger.warning("In sandbox mode, unable to make deposit")
    else:
        logger.info("Deposit Response: {}".format(resp))

    return resp
Enter fullscreen mode Exit fullscreen mode

If you run your app now (heroku local) and head to the '/deposits' endpoint, you'll see the same error you saw in the previous section. However, if you head to your S3 bucket, open the folder with today's date, and open the file named "deposit_funds_[todays_date].log", you should see something like this.
log screenshot

Yay! Now we can see where in the function the error is coming from. When you deploy the app to Heroku and use your production credentials, the log message will show you the response for making the deposit.

That's (almost) it! Commit your changes and push to your repo so it can make its way to Heroku.

Environment Variables in Heroku

Remember how we created a .env file so that we could read environment variables when using heroku local? Well, now that it's game time, it's time to add our real credentials to our Heroku app. On your app's Settings page in Heroku, click on "Reveal Config Vars" and add the following key value pairs.

  • key: CB_KEY value: YOUR_PRODUCTION_CB_KEY
  • key: CB_PASSPHRASE value: YOUR_PRODUCTION_PASSPHRASE
  • key: CB_SECRET value: YOUR_PRODUCTION_SECRET
  • key: CB_URL value: https://api.pro.coinbase.com
  • key: AWS_ACCESS_KEY_ID value: YOUR_AWS_ACCESS_KEY
  • key: AWS_SECRET_ACCESS_KEY value: YOUR_AWS_SECRET_KEY
  • key: TZ value: Your Timezone (e.g. America/New_York) (optional)

Now, when your app runs, it will use these credentials.

Schedule

If you've made it this far, congrats! You now have an app that makes a deposit of $10 whenever you go to http://your_domain_url.com/deposit. But what if you want the app to do it automatically? Next, I'll show you how to make some slight changes so that the deposit_funds function runs on a schedule.

Alright, one more package to install. In your terminal run the following:

pip install flask_script
pip freeze > requirements.txt 
Enter fullscreen mode Exit fullscreen mode

Then create the following file.
Filename: manage.py

from flask_script import Manager

from app import app
from deposit_funds import deposit_funds

manager = Manager(app)

@manager.command
def make_deposit():
    deposit_funds()
Enter fullscreen mode Exit fullscreen mode

This file creates a command that we can call from Heroku.

Then from the Resources tab on your Heroku dashboard, search for Heroku Scheduler and add it to your app. Note: Heroku might ask you to add payment information to your account. Click on "Heroku Scheduler" and then "Create job". I want my function to run every Thursday. Here's how my job looks
heroku job

Run Command: if [ "$(date +%u)" = 4 ]; then python manage.py make_deposit; fi

Click "Save Job" to lock it in. You're done! You now have an app that will make automatic deposits to your Coinbase Pro account and log activities to a S3 bucket!

Extra notes

Another alternative is to create an AWS Lambda function and that gets called via AWS Cloudwatch. If you're interested in seeing how to do it this way, let me know!

Top comments (2)

Collapse
 
sbadisa profile image
sbadisa

Thanks for this great tutorial. It's been a massive help. I do have a question though - where does the os come from in the config.py file? I don't see it defined anywhere...

Collapse
 
sbadisa profile image
sbadisa

Solved this problem by importing os. But now I have a different error:

'PASSPHRASE': os.environ['CB_PASSPHRASE'],
Enter fullscreen mode Exit fullscreen mode

File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/os.py", line 679, in getitem
raise KeyError(key) from None
KeyError: 'CB_PASSPHRASE'

Any ideas on what I'm missing? I've followed the tutorial to the letter :S