-
-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8a844f2
commit 90b945c
Showing
11 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,3 +99,5 @@ ENV/ | |
|
||
# mypy | ||
.mypy_cache/ | ||
|
||
coinmarketcap_cache.sqlite |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
FROM python:3 | ||
|
||
WORKDIR /usr/src/app | ||
|
||
COPY requirements.txt ./ | ||
RUN pip install --no-cache-dir -r requirements.txt | ||
|
||
COPY optimal-buy-gdax.py . | ||
|
||
ENTRYPOINT [ "python", "./optimal-buy-gdax.py" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,79 @@ | ||
# optimal-buy-gdax | ||
Scheduled buying of BTC, ETH, and LTC from GDAX optimally! | ||
|
||
 | ||
|
||
# What is this? | ||
|
||
This is a Python script you can use to automatically buy Bitcoin, Ethereum, | ||
and Litecoin using the GDAX API. It buys these 3 currencies, weighted by market | ||
cap (as reported by [coinmarketcap.com](https://coinmarketcap.com/)), using a form of [dollar cost averaging](https://www.bogleheads.org/wiki/Dollar_cost_averaging). according | ||
to the following logic: | ||
|
||
1. Check current balances of USD, BTC, ETH, and LTC | ||
1. If the USD balance is above $100, buy BTC, ETH, and LTC weighted by market cap | ||
1. Place 5 discounted limit orders at the current price minus 0.55% up to 0.55%, | ||
each order with 1/5th of the remaining amount to buy for each coin | ||
1. If the USD account balance is below $100, withdraw coins to desired addresses | ||
|
||
You can also use the same script to schedule deposits from your bank account | ||
periodically, such as when you're paid. | ||
|
||
Ideally, this script would help to make sure that when we dip— | ||
|
||
 | ||
|
||
**we buy**. | ||
|
||
# USE AT OWN RISK | ||
|
||
Duh. Not my fault if you lose everything. | ||
|
||
# How do I use it? | ||
|
||
1. Set up a GDAX account, and link your bank account | ||
1. Create the necessary API credentials for GDAX, with permissions to | ||
manage funds, withdraw without 2FA, and trade | ||
1. Determine the payment_method_id value by using the [GDAX API](https://docs.gdax.com/#payment-methods) (you can use your browser's developer toolbar) | ||
1. Get a machine somewhere (GCE, EC2, Digital Ocean) with Docker and systemd | ||
1. Copy systemd files over: | ||
|
||
$ sudo cp optimal-buy-gdax-*.{service,timer} /etc/systemd/system | ||
1. Edit [`/etc/systemd/system/optimal-buy-gdax-buy.service`](optimal-buy-gdax-buy.service), | ||
[`/etc/systemd/system/optimal-buy-gdax-buy.timer`](optimal-buy-gdax-buy.timer), | ||
[`/etc/systemd/system/optimal-buy-gdax-deposit.service`](optimal-buy-gdax-deposit.service), and | ||
[`/etc/systemd/system/optimal-buy-gdax-deposit.timer`](optimal-buy-gdax-deposit.timer) to your liking. Make sure you: | ||
|
||
* Change the BTC, ETH, and LTC deposit addresses | ||
* Put the correct API keys in | ||
* Check the deposit amount | ||
* Check the timer dates | ||
|
||
1. Enable the systemd units: | ||
|
||
$ sudo systemctl enable optimal-buy-gdax-buy.service | ||
$ sudo systemctl enable optimal-buy-gdax-buy.timer | ||
$ sudo systemctl enable optimal-buy-gdax-deposit.service | ||
$ sudo systemctl enable optimal-buy-gdax-deposit.timer | ||
|
||
1. Start the systemd timers: | ||
$ sudo systemctl start optimal-buy-gdax-buy.timer | ||
$ sudo systemctl start optimal-buy-gdax-deposit.timer | ||
|
||
1. Enjoy! | ||
|
||
# Caveats/limitations | ||
|
||
* Currently it only supports USD | ||
* Some values are hardcoded, but if you want to change that feel free to send a | ||
PR! | ||
* If you try to trade manually or using some other bot at the same time, | ||
you're probably going to have a bad time | ||
* You might have a few dollars (<$100) sitting in your account at all times, even when all orders have been filled | ||
|
||
# Tipjar | ||
|
||
If you got some value out of this, please send some kudos my way: | ||
|
||
* BTC: 37N55ywGVJZsKCsfCmkfDvwukN9GJi93vK | ||
* LTC: 328FYhK7ioQphpsHsQFyDb55yhkEkG6mgf |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
[Unit] | ||
Description=optimal-buy-gdax-buy | ||
Wants=optimal-buy-gdax-buy.timer | ||
After=docker.service | ||
Requires=docker.service | ||
|
||
[Service] | ||
TimeoutStartSec=0 | ||
ExecStartPre=-/usr/bin/docker stop optimal-buy-gdax | ||
ExecStartPre=-/usr/bin/docker rm optimal-buy-gdax | ||
ExecStartPre=/usr/bin/docker pull brndnmtthws/optimal-buy-gdax | ||
ExecStart=/usr/bin/docker run --name optimal-buy-gdax \ | ||
brndnmtthws/optimal-buy-gdax \ | ||
--key myapikey \ | ||
--b64secret mysecret \ | ||
--passphrase mypassphrase \ | ||
--mode buy \ | ||
--btc-addr 37N55ywGVJZsKCsfCmkfDvwukN9GJi93vK \ | ||
--eth-addr 0xb794F5abcde39494cE839613fffBA74279579268 \ | ||
--ltc-addr 328FYhK7ioQphpsHsQFyDb55yhkEkG6mgf | ||
|
||
[Install] | ||
WantedBy=optimal-buy-gdax-buy.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[Unit] | ||
Description=GDAX buy order timer | ||
|
||
[Timer] | ||
OnCalendar=*-*-* 00:28:00 | ||
Unit=optimal-buy-gdax-buy.service | ||
|
||
[Install] | ||
WantedBy=multi-user.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[Unit] | ||
Description=optimal-buy-gdax-deposit | ||
Wants=optimal-buy-gdax-deposit.timer | ||
After=docker.service | ||
Requires=docker.service | ||
|
||
[Service] | ||
TimeoutStartSec=0 | ||
ExecStartPre=-/usr/bin/docker stop optimal-buy-gdax | ||
ExecStartPre=-/usr/bin/docker rm optimal-buy-gdax | ||
ExecStartPre=/usr/bin/docker pull brndnmtthws/optimal-buy-gdax | ||
ExecStart=/usr/bin/docker run --name optimal-buy-gdax \ | ||
brndnmtthws/optimal-buy-gdax \ | ||
--key myapikey \ | ||
--b64secret mysecret \ | ||
--passphrase mypassphrase \ | ||
--mode deposit \ | ||
--amount 1000 \ | ||
--payment-method-id e49c8d15-547b-464e-ac3d-4b9d20b360ec | ||
|
||
[Install] | ||
WantedBy=optimal-buy-gdax-deposit.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[Unit] | ||
Description=GDAX deposit timer | ||
|
||
[Timer] | ||
OnCalendar=Mon *-*-* 00:00:00 | ||
Unit=optimal-buy-gdax-deposit.service | ||
|
||
[Install] | ||
WantedBy=multi-user.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import gdax | ||
import argparse | ||
import sys | ||
from coinmarketcap import Market | ||
|
||
parser = argparse.ArgumentParser(description='Buy coins!') | ||
parser.add_argument('--mode', | ||
help='mode (deposit or buy)', required=True) | ||
parser.add_argument('--amount', type=float, help='amount to deposit') | ||
parser.add_argument('--key', help='API key', required=True) | ||
parser.add_argument('--b64secret', help='API secret', required=True) | ||
parser.add_argument('--passphrase', help='API passphrase', required=True) | ||
parser.add_argument('--api-url', help='API URL', | ||
default='https://api.gdax.com') | ||
parser.add_argument('--payment-method-id', | ||
help='Payment method ID for USD deposit') | ||
parser.add_argument('--btc-addr', help='BTC withdrawal address') | ||
parser.add_argument('--eth-addr', help='ETH withdrawal address') | ||
parser.add_argument('--ltc-addr', help='LTC withdrawal address') | ||
|
||
args = parser.parse_args() | ||
|
||
coins = { | ||
'BTC': 'Bitcoin', | ||
'ETH': 'Ethereum', | ||
'LTC': 'Litecoin', | ||
} | ||
|
||
client = gdax.AuthenticatedClient(args.key, args.b64secret, args.passphrase, | ||
args.api_url) | ||
|
||
|
||
def get_weights(): | ||
coinmarketcap = Market() | ||
|
||
market_cap = {} | ||
for c in coins: | ||
ticker = coinmarketcap.ticker(currency=coins[c]) | ||
market_cap[c] = float(ticker[0]['market_cap_usd']) | ||
|
||
total_market_cap = sum(market_cap.values()) | ||
|
||
weights = {} | ||
for c in coins: | ||
weights[c] = market_cap[c] / total_market_cap | ||
print('Coin weights:') | ||
for w in weights: | ||
print(' {0}: {1:.4f}'.format(w, weights[w])) | ||
print() | ||
return weights | ||
|
||
|
||
def deposit(): | ||
if args.amount is None: | ||
print('Please specify deposit amount with `--amount`') | ||
sys.exit(1) | ||
if args.payment_method_id is None: | ||
print('Please provide a bank ID with `--payment-method-id`') | ||
sys.exit(1) | ||
print('Performing deposit, amount={} USD'.format(args.amount)) | ||
result = client.deposit(payment_method_id=args.payment_method_id, | ||
amount=args.amount, | ||
currency='USD') | ||
print(result) | ||
|
||
|
||
def get_balance_for(accounts, coin): | ||
for a in accounts: | ||
if a['currency'] == coin: | ||
return float(a['balance']) | ||
return 0 | ||
|
||
|
||
def get_prices(): | ||
prices = {} | ||
for c in coins: | ||
ticker = client.get_product_ticker(product_id=c + '-USD') | ||
prices[c] = float(ticker['price']) | ||
return prices | ||
|
||
|
||
def get_usd_balances(accounts, prices): | ||
balances = {} | ||
for a in accounts: | ||
if a['currency'] == 'USD': | ||
balances['USD'] = float(a['balance']) | ||
elif a['currency'] in coins: | ||
balances[a['currency']] = \ | ||
float(a['balance']) * prices[a['currency']] | ||
for c in coins: | ||
if c not in balances: | ||
balances[c] = 0 | ||
return balances | ||
|
||
|
||
def get_account(accounts, currency): | ||
for a in accounts: | ||
if a['currency'] == currency: | ||
return a | ||
|
||
|
||
def place_buy_orders(balance_difference_usd, coin, price): | ||
if balance_difference_usd <= 0.1: | ||
print('Difference for {} is <= 0.1, skipping'.format(coin)) | ||
return | ||
|
||
# Set 5 buy orders, in 1% discount increments, starting from 5.5% off | ||
remaining_usd = balance_difference_usd | ||
discount = 0.945 | ||
amount = remaining_usd / 5.0 | ||
for i in range(0, 5): | ||
discount = discount + 0.01 | ||
discounted_price = price * discount | ||
size = amount / (discounted_price) | ||
order = client.buy( | ||
price='{0:.2f}'.format(discounted_price), | ||
size='{0:.8f}'.format(size), | ||
type='limit', | ||
product_id=coin + '-USD', | ||
) | ||
print('order={}'.format(order)) | ||
if remaining_usd <= 0.01: | ||
break | ||
|
||
|
||
|
||
def start_buy_orders(accounts, prices, usd_balances): | ||
weights = get_weights() | ||
|
||
# Determine amount of each coin, in USD, to buy | ||
usd_balance_sum = sum(usd_balances.values()) | ||
print('usd_balance_sum={}'.format(usd_balance_sum)) | ||
|
||
target_amount_usd = {} | ||
for c in coins: | ||
target_amount_usd[c] = usd_balance_sum * weights[c] | ||
print('target_amount_usd={}'.format(target_amount_usd)) | ||
|
||
balance_differences_usd = {} | ||
for c in coins: | ||
balance_differences_usd[c] = target_amount_usd[c] - usd_balances[c] | ||
print('balance_differences_usd={}'.format(balance_differences_usd)) | ||
|
||
for c in coins: | ||
place_buy_orders(balance_differences_usd[c], c, prices[c]) | ||
|
||
|
||
def withdraw(accounts): | ||
# Check that we've got addresses | ||
if args.btc_addr is None: | ||
print('No BTC withdraw address specified with `--btc-addr`') | ||
if args.eth_addr is None: | ||
print('No ETH withdraw address specified with `--eth-addr`') | ||
if args.ltc_addr is None: | ||
print('No LTC withdraw address specified with `--ltc-addr`') | ||
|
||
# BTC | ||
btc_account = get_account(accounts, 'BTC') | ||
transaction = client.crypto_withdraw( | ||
amount=btc_account['balance'], | ||
currency='BTC', | ||
crypto_address=args.btc_addr | ||
) | ||
print('transaction={}'.format(transaction)) | ||
|
||
# ETH | ||
eth_account = get_account(accounts, 'ETH') | ||
transaction = client.crypto_withdraw( | ||
amount=eth_account['balance'], | ||
currency='ETH', | ||
crypto_address=args.eth_addr | ||
) | ||
print('transaction={}'.format(transaction)) | ||
|
||
# LTC | ||
ltc_account = get_account(accounts, 'LTC') | ||
transaction = client.crypto_withdraw( | ||
amount=ltc_account['balance'], | ||
currency='LTC', | ||
crypto_address=args.ltc_addr | ||
) | ||
print('transaction={}'.format(transaction)) | ||
|
||
|
||
def buy(): | ||
print('Starting buy and (maybe) withdrawal') | ||
print('First, cancelling orders') | ||
for c in coins: | ||
client.cancel_all(product=c + '-USD') | ||
# Check if there's any USD available to execute a buy | ||
accounts = client.get_accounts() | ||
prices = get_prices() | ||
print('accounts={}'.format(accounts)) | ||
print('prices={}'.format(prices)) | ||
|
||
usd_balances = get_usd_balances(accounts, prices) | ||
print('usd_balances={}'.format(usd_balances)) | ||
|
||
if usd_balances['USD'] > 100: | ||
print('USD balance above $100, buying more') | ||
start_buy_orders(accounts, prices, usd_balances) | ||
else: | ||
print('Only {} USD balance remaining, withdrawing' | ||
' coins without buying'.format(usd_balances['USD'])) | ||
withdraw(accounts) | ||
|
||
|
||
if args.mode == 'deposit': | ||
deposit() | ||
elif args.mode == 'buy': | ||
buy() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
gdax | ||
requests | ||
coinmarketcap |