Skip to content
This repository has been archived by the owner on Nov 27, 2022. It is now read-only.

Commit

Permalink
Update/fastapi cloudrun cloudscheduler (#1)
Browse files Browse the repository at this point in the history
* docker updates

* deployment

* cloudrun docker needs gunicorn

* cool

* 0.1.0

* doc: update
  • Loading branch information
anthonycorletti committed Jan 8, 2022
1 parent 8c54fc1 commit c27d389
Show file tree
Hide file tree
Showing 16 changed files with 270 additions and 94 deletions.
5 changes: 3 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ tmp/

.git
.github
docs
kubernetes
scripts
tests
.coveragerc
Expand All @@ -158,3 +156,6 @@ tests
mypy.ini
minikube-darwin-amd64
.idea
configs
examples
docs
23 changes: 0 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,3 @@ pyenv rehash
```sh
./scripts/test.sh
```

## Installing and Running

First, create your config file. You can find examples in the [examples dir](./examples).

You will need to create a Coinbase Pro API Key and, optionally, a discord webhook.

- [Create a Coinbase API Key](https://help.coinbase.com/en/pro/other-topics/api/how-do-i-create-an-api-key-for-coinbase-pro)
- [Create a Discord webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks)

Then, after you filled in all the values in your `config.yaml`, install and run `cbpa`

```sh
pip install cbpa
cbpa run -f my-config.yaml
```

To run with docker;

```sh
./scripts/docker-build.sh
./scripts/docker-run.sh cbpa --help
```
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
FROM python:3.9.6-slim

WORKDIR /cbpa
ENV PYTHONUNBUFFERED True

WORKDIR /cbpa
COPY . /cbpa

RUN apt-get update -y \
Expand All @@ -10,3 +11,5 @@ RUN apt-get update -y \
&& pip install flit \
&& FLIT_ROOT_INSTALL=1 flit install --deps production \
&& rm -rf $(pip cache dir)

CMD gunicorn cbpa.main:api -c cbpa/gunicorn_config.py
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,66 @@

Coinbase Pro Automation for making buy orders from a default bank account.

Checkout the [contributing guide](./CONTRIBUTING.md) to get started.
## Quickstart

1. Install with pip

```sh
pip install cbpa
```

1. [Create a Coinbase API Key](https://help.coinbase.com/en/pro/other-topics/api/how-do-i-create-an-api-key-for-coinbase-pro), you will need to select all fields of access.

1. (Optional). [Create a Discord webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks).

1. Make your config file. See the [examples](./examples) for more.

```yaml
---
api:
key: "myKey"
secret: "mySecret"
passphrase: "myPassphrase"
url: "https://api.pro.coinbase.com"
discord:
webhook: https://discord.com/api/webhooks/123/abc
account:
auto_funding_limit: 20
fiat_currency: USD
buys:
- send_currency: USD
send_amount: 2
receive_currency: BTC
- send_currency: USD
send_amount: 2
receive_currency: ETH
- send_currency: USD
send_amount: 2
receive_currency: DOGE
```

1. Make your orders!

```sh
cbpa run -f my-buys.yaml
```

## Running `cbpa` in Google Cloud Run

You can run `cbpa` as a server in Google Cloud Run, which can called by Google Cloud Scheduler to automatically place buys for you each day, or on any cron schedule you like.

These steps assume you have installed and configured `gcloud` already.

1. Store your buy order file as a secret in GCP.

```sh
gcloud secrets versions add my_buys --data-file=my-buys.yaml
```

1. Build and push your docker container to Google Cloud, and then deploy your container.

```sh
./scripts/docker-build.sh && ./scripts/docker-push.sh; SECRET_ID=my_buys ./scripts/gcloud-run-deploy.sh
```

1. [Create an authenticated scheduler](https://cloud.google.com/scheduler/docs/http-target-auth#using-the-console) that uses an http target to hit the `buy` endpoint.
2 changes: 1 addition & 1 deletion cbpa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""cbpa"""

__version__ = "0.0.2"
__version__ = "0.1.0"
8 changes: 8 additions & 0 deletions cbpa/gunicorn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""gunicorn server configuration."""
import os

workers = 1
threads = 8
timeout = 0
bind = f":{os.environ.get('PORT', '8002')}"
worker_class = "uvicorn.workers.UvicornWorker"
164 changes: 103 additions & 61 deletions cbpa/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import os
import traceback
from datetime import datetime
from typing import Optional

import coinbasepro
import typer
import uvicorn
import yaml
from fastapi import FastAPI
from google.cloud import secretmanager

from cbpa import __version__
from cbpa.logger import logger
from cbpa.schemas.buy import BuyResponse
from cbpa.schemas.config import Config
from cbpa.schemas.health import HealthcheckResponse
from cbpa.services.account import AccountService
from cbpa.services.buy import BuyService
from cbpa.services.config import ConfigService
from cbpa.services.discord import DiscordService

os.environ["TZ"] = "UTC"
app = typer.Typer(name="Coinbase Pro Automation")


def create_config(filepath: str) -> Config:
Expand All @@ -25,6 +30,16 @@ def create_config(filepath: str) -> Config:
return config


def retrieve_config_from_secret_manager(
project_id: str, secret_id: str, version_id: str = "latest"
) -> Config:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
response = client.access_secret_version(request={"name": name})
payload = response.payload.data.decode("UTF-8")
return Config(**yaml.safe_load(payload))


def create_coinbasepro_auth_client(config: Config) -> coinbasepro.AuthenticatedClient:
logger.info("📡 Connecting to Coinbase Pro.")
client = coinbasepro.AuthenticatedClient(
Expand All @@ -50,33 +65,22 @@ def create_account_service(


def create_buy_service(
config: Config, coinbasepro_client: coinbasepro.AuthenticatedClient
config: Config,
coinbasepro_client: coinbasepro.AuthenticatedClient,
account_service: AccountService,
) -> BuyService:
logger.info("💸 Creating buy service.")
buy_service = BuyService(
config=config,
coinbasepro_client=coinbasepro_client,
account_service=account_service,
)
logger.info("👏 Successfully created buy service.")
return buy_service


@app.command("version", help="prints the version")
def _version() -> None:
typer.echo(__version__)


@app.command("run", help="executes buy orders listed in a config file")
def _run(
filepath: str = typer.Option(
...,
"-f",
"--file",
help="filepath to the yaml config",
)
) -> None:
def _main(config: Config) -> None:
start = datetime.now()
config = create_config(filepath=filepath)
discord_service = DiscordService()
start_message = f"🤖 Starting cbpa ({start.isoformat()})"
logger.info(start_message)
Expand All @@ -88,54 +92,92 @@ def _run(
buy_service = create_buy_service(
config=config,
coinbasepro_client=coinbasepro_client,
account_service=account_service,
)
done = {buy.receive_currency.value: False for buy in config.buys}

def run() -> None:
try:
for buy in config.buys:
if not done[buy.receive_currency.value]:
buy_total = buy.send_amount
current_funds = account_service.get_balance_for_currency(
config.account.fiat_currency
)
if current_funds >= buy_total:
buy_service.place_market_order(
buy, config.account.fiat_currency
)
done[buy.receive_currency.value] = True
elif current_funds < buy_total:
response = account_service.add_funds(
buy_total=buy_total,
current_funds=current_funds,
max_fund=config.account.auto_funding_limit,
fiat=config.account.fiat_currency,
)
if response.status == "Error":
logger.error(response.message)
discord_service.send_alert(
config=config, message=response.message
)
elif response.status == "Success":
buy_service.place_market_order(
buy, config.account.fiat_currency
)
done[buy.receive_currency.value] = True
else:
logger.info(
f"Unhandled response status {response.status}."
" Moving on."
)
if not all(done.values()):
run()
except Exception:
logger.error("Unhandled general exception occurred.")
logger.error(traceback.format_exc())
run()

run()
buy_service.run(done=done)

end = datetime.now()
duration = end - start
end_message = f"🤖 cbpa completed! Ran for {duration.total_seconds()} seconds."
logger.info(end_message)
discord_service.send_alert(config=config, message=end_message)


#
# create the api
#
api = FastAPI(
title="Coinbase Pro Automation API",
description="Automate buys for your favourite cryptocurrencies.",
version=__version__,
)


#
# create api routes
#
@api.get("/healthcheck", response_model=HealthcheckResponse, tags=["health"])
def healthcheck() -> HealthcheckResponse:
message = "Dollar cost averaging."
logger.debug(message)
return HealthcheckResponse(
api_version=__version__,
message=message,
time=datetime.now(),
)


@api.post("/buy", response_model=BuyResponse, tags=["buy"])
def buy() -> BuyResponse:
logger.info("Buy API request initiated.")
config = retrieve_config_from_secret_manager(
project_id=os.environ["PROJECT_ID"], secret_id=os.environ["SECRET_ID"]
)
_main(config=config)
logger.info("Buy API request completed.")
return BuyResponse(message="Buy API request completed.")


#
# create the cli
#
typer_app_name = "Coinbase Pro Automation"
app = typer.Typer(name=typer_app_name)


#
# create cli commands
#
@app.command("version", help="prints the version")
def _version() -> None:
typer.echo(__version__)


@app.command("run", help="executes buy orders listed in a config file")
def _run(
filepath: str = typer.Option(
...,
"-f",
"--file",
help="filepath to the yaml config",
)
) -> None:
config = create_config(filepath=filepath)
_main(config=config)


@app.command("server", help="run an api server to handle automated buys")
def _server(
port: Optional[str] = typer.Option(
None, "--port", "-p", help="the port to run uvicon on"
),
host: str = typer.Option(
"0.0.0.0", "--host", "-h", help="the port to run uvicon on"
),
) -> None:
if port is None:
port = os.getenv("PORT", "8002")
assert port is not None
uvicorn.run(api, port=int(port), host=host)
4 changes: 4 additions & 0 deletions cbpa/schemas/buy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ class Buy(BaseModel):

def pair(self) -> str:
return f"{self.receive_currency}-{self.send_currency}"


class BuyResponse(BaseModel):
message: str
9 changes: 9 additions & 0 deletions cbpa/schemas/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from datetime import datetime

from pydantic import BaseModel


class HealthcheckResponse(BaseModel):
api_version: str
message: str
time: datetime
Loading

0 comments on commit c27d389

Please sign in to comment.