Skip to content

Commit

Permalink
Merge pull request #6 from FuturBroke/multiple_pairs
Browse files Browse the repository at this point in the history
Multiple pairs support
  • Loading branch information
adocquin authored Sep 12, 2021
2 parents c28c0ae + ca0e870 commit 3130b24
Show file tree
Hide file tree
Showing 16 changed files with 1,122 additions and 100 deletions.
41 changes: 25 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Kraken-DCA
Kraken-DCA is a python program to automate
[Dollar Cost Averaging](https://www.investopedia.com/terms/d/dollarcostaveraging.asp) on
[Kraken](https://kraken.com) exchange.<br>
At every launch, if no DCA pair order was already passed for the pair and delay in configuration
file, it will create a buy limit order at current pair ask price for the specified amount.
Kraken-DCA is a python program to automate pairs
[Dollar Cost Averaging](https://www.investopedia.com/terms/d/dollarcostaveraging.asp)
on as many pairs as you want on [Kraken](https://kraken.com) exchange.<br>
At every launch, if no DCA pair order was already passed for each pair and delay in
configuration file, it will create a buy limit order at current pair ask price for the specified amount.

Order history is saved in CSV format

Expand Down Expand Up @@ -61,7 +61,7 @@ Order history is saved in CSV format with following information per order:
Order history is by default saved in *orders.csv* in Kraken-DCA base directory,
the output file can be changed through docker image execution as described below.

# Usage
# How to run it
## Configuration file
If you don't use docker you must edit the default *config.yaml* file.

Expand All @@ -71,15 +71,23 @@ api:
public_key: "KRAKEN_API_PUBLIC_KEY"
private_key: "KRAKEN_API_PRIVATE_KEY"

# DCA days delay, pair to DCA and corresponding amount to buy.
dca:
delay: 2
pair: "XETHZEUR"
amount: 20
# DCA pairs configuration. You can add as many pairs as you want.
# pair: Name of the pair (list of available pairs: https://api.kraken.com/0/public/AssetPairs)
# delay: Delay in days between each buy limit order.
# amount: Amount of the order in quote asset.
dca_pairs:
- pair: "XETHZEUR"
delay: 1
amount: 15
- pair: "XXBTZEUR"
delay: 3
amount: 20
```
- In api, public_key and private_key correspond to your Kraken API key information.
- Delay is the number of days between buy orders. Set to 1 to DCA each day, 7 once per week.
- Available pairs for pair field can be found [here](https://api.kraken.com/0/public/AssetPairs) on *altname*.
- Amount of quote asset to sell to buy base asset.
- Amount is the amount of quote asset to sell to buy base asset.
- You can specify as many pairs as you want in the dca_pairs list.
More information on
[Kraken API official documentation](https://support.kraken.com/hc/en-us/articles/360000920306-Ticker-pairs).
Expand Down Expand Up @@ -127,7 +135,7 @@ python __main__.py
```
### Automate DCA through cron
You can automate the execution by using cron on unix systems.
To execute the program every hour (it will only buy if no DCA air order was done the current day) run in a shell:
To execute the program every hour (it will only buy if no DCA pair order was done the current day) run in a shell:
```sh
crontab -e
```
Expand All @@ -151,9 +159,10 @@ More crontab execution frequency options: https://crontab.guru/
[GPL-3.0](https://github.com/FuturBroke/kraken-dca/blob/main/README.md)

# How to contribute
Thanks for your interest in contributing to the project. You can contribute freely by creating an issue, fork or create
a pull request. Before issuing a pull request, make sure the changes did not break any existing functionality by
running unit tests in the base directory:
Thanks for your interest in contributing to the project. You can contribute freely by
creating an issue, fork or create a pull request. Before issuing a pull request, make
sure the changes did not break any existing functionality by running unit tests in the
base directory:
```sh
pytest
```
25 changes: 10 additions & 15 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from krakendca import Config, Pair, DCA
from krakendca import Config, KrakenDCA
from krakenapi import KrakenApi


if __name__ == "__main__":
try:
# Get parameters form configuration file.
config = Config("config.yaml")
# Initialize the KrakenAPI object.
ka = KrakenApi(config.api_public_key, config.api_private_key)
# Initialize the Pair object from pair specified in
# configuration file and data from Kraken.
pair = Pair.get_pair_from_kraken(ka, config.pair)
# Initialize the DCA object.
dca = DCA(ka, config.delay, pair, config.amount)
# Execute DCA logic.
dca.handle_dca_logic()
except Exception as e:
print(e)
# Get parameters from configuration file.
config = Config("config.yaml")
# Initialize the KrakenAPI object.
ka = KrakenApi(config.api_public_key, config.api_private_key)
# Initialize the KrakenDCA object and handle the DCA based on configuration.
kdca = KrakenDCA(config, ka)
kdca.initialize_pairs_dca()
kdca.handle_pairs_dca()

17 changes: 12 additions & 5 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ api:
public_key: "KRAKEN_API_PUBLIC_KEY"
private_key: "KRAKEN_API_PRIVATE_KEY"

# DCA days delay, pair to DCA and corresponding amount to buy.
dca:
delay: 1
pair: "XETHZEUR"
amount: 20
# DCA pairs configuration. You can add as many pairs as you want.
# pair: Name of the pair (list of available pairs: https://api.kraken.com/0/public/AssetPairs)
# delay: Delay in days between each buy limit order.
# amount: Amount of the order in quote asset.
dca_pairs:
- pair: "XETHZEUR"
delay: 1
amount: 15
- pair: "XXBTZEUR"
delay: 3
amount: 20

1 change: 1 addition & 0 deletions krakendca/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .config import Config
from .dca import DCA
from .krakendca import KrakenDCA
from .order import Order
from .pair import Pair
from .utils import (
Expand Down
76 changes: 52 additions & 24 deletions krakendca/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import yaml
from yaml.scanner import ScannerError

CONFIG_ERROR_MSG: str = "Configuration file incorrectly formatted"


class Config:
Expand All @@ -8,9 +11,7 @@ class Config:

api_public_key: str
api_private_key: str
delay: int
pair: str
amount: float
dca_pairs: list

def __init__(self, config_file: str) -> None:
"""
Expand All @@ -20,26 +21,53 @@ def __init__(self, config_file: str) -> None:
"""
try:
with open(config_file, "r") as stream:
try:
config = yaml.load(stream, Loader=yaml.SafeLoader)
self.api_public_key = config.get("api").get("public_key")
self.api_private_key = config.get("api").get("private_key")
self.delay = config.get("dca").get("delay")
self.pair = config.get("dca").get("pair")
self.amount = float(config.get("dca").get("amount"))
except (ValueError, TypeError, AttributeError, yaml.YAMLError) as e:
raise ValueError(f"Configuration file incorrectly formatted: {e}")
config = yaml.load(stream, Loader=yaml.SafeLoader)
self.api_public_key = config.get("api").get("public_key")
self.api_private_key = config.get("api").get("private_key")
self.dca_pairs = config.get("dca_pairs")
self.__check_configuration()
for dca_pair in self.dca_pairs:
self.__check_dca_pair_configuration(dca_pair)
except EnvironmentError:
raise FileNotFoundError("Configuration file not found.")
if not self.api_public_key:
raise TypeError("Please provide your Kraken API public key.")
elif not self.api_private_key:
raise TypeError("Please provide your Kraken API private key.")
elif not self.delay or type(self.delay) is not int or self.delay <= 0:
raise TypeError("Please set the DCA days delay as a number > 0.")
elif not self.pair:
raise TypeError("Please provide the pair to dollar cost average.")
elif not self.amount or type(self.amount) is not float or self.amount <= 0:
raise TypeError(
"Please provide an amount > 0 to daily dollar cost average."
)
except ScannerError as e:
raise ScannerError(CONFIG_ERROR_MSG + f": {e}")

def __check_configuration(self):
"""
Check Config attributes and raise an error in case of missing
parameters in configuration file.
"""
try:
if not self.api_public_key:
raise ValueError("Please provide your Kraken API public key.")
if not self.api_private_key:
raise ValueError("Please provide your Kraken API private key.")
if not self.dca_pairs or type(self.dca_pairs) is not list:
raise ValueError("No DCA pairs specified.")
except ValueError as e:
raise ValueError(CONFIG_ERROR_MSG + f": {e}")

@staticmethod
def __check_dca_pair_configuration(dca_pair: dict):
"""
Check DCA pair configuration parameters are currently specified.
:param dca_pair: Dictionary with pair to DCA, delay in days as integer and
amount of quote asset to make the limit buy order with.
"""
try:
if not dca_pair.get("pair"):
raise ValueError("Please provide the pair to dollar cost average.")
delay = dca_pair.get("delay")
if not delay or type(delay) is not int or delay <= 0:
raise ValueError("Please set the DCA days delay as a number > 0.")
try:
dca_pair["amount"] = float(dca_pair.get("amount"))
except TypeError:
raise ValueError("Please provide an amount > 0 to DCA.")
amount = dca_pair.get("amount")
if not amount or type(amount) is not float or amount <= 0:
raise ValueError("Please provide an amount > 0 to DCA.")
except ValueError as e:
raise ValueError(CONFIG_ERROR_MSG + f": {e}")
22 changes: 13 additions & 9 deletions krakendca/dca.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ def __init__(
self.pair = pair
self.amount = float(amount)
self.orders_filepath = orders_filepath
print(
f"Hi, current configuration: DCA pair: {self.pair.name}, DCA amount: {self.amount}."
)
print(f"Pair: {self.pair.name}, delay: {self.delay}, amount: {self.amount}.")

def handle_dca_logic(self) -> None:
"""
Expand Down Expand Up @@ -95,7 +93,8 @@ def get_system_time(self) -> datetime:
lag = (current_date - kraken_date).seconds
if lag > 1:
raise OSError(
"Too much lag -> Check your internet connection speed or synchronize your system time."
"Too much lag -> Check your internet connection speed "
"or synchronize your system time."
)
return current_date

Expand All @@ -119,11 +118,13 @@ def check_account_balance(self) -> None:
except TypeError: # When there is no pair quote balance on Kraken account.
pair_quote_balance = 0
print(
f"Pair balances: {pair_quote_balance} {self.pair.quote}, {pair_base_balance} {self.pair.base}."
f"Pair balances: {pair_quote_balance} {self.pair.quote}, "
f"{pair_base_balance} {self.pair.base}."
)
if pair_quote_balance < self.amount:
raise ValueError(
f"Insufficient funds to buy {self.amount} {self.pair.quote} of {self.pair.base}"
f"Insufficient funds to buy {self.amount} "
f"{self.pair.quote} of {self.pair.base}"
)

def count_pair_daily_orders(self) -> int:
Expand Down Expand Up @@ -178,14 +179,17 @@ def send_buy_limit_order(self, order: Order) -> None:
"""
if order.volume < self.pair.order_min:
raise ValueError(
f"Too low volume to buy {self.pair.base}: current {order.volume}, minimum {self.pair.order_min}."
f"Too low volume to buy {self.pair.base}: current {order.volume}, "
f"minimum {self.pair.order_min}."
)
print(
f"Create a {order.price}{self.pair.quote} buy limit order of {order.volume}{self.pair.base} at {order.pair_price}{self.pair.quote}."
f"Create a {order.price}{self.pair.quote} buy limit order of "
f"{order.volume}{self.pair.base} at {order.pair_price}{self.pair.quote}."
)
print(f"Fee expected: {order.fee}{self.pair.quote} (0.26% taker fee).")
print(
f"Total price expected: {order.volume}{self.pair.base} for {order.total_price}{self.pair.quote}."
f"Total price expected: {order.volume}{self.pair.base} for "
f"{order.total_price}{self.pair.quote}."
)
order.send_order(self.ka)
print(f"Order successfully created.")
Expand Down
48 changes: 48 additions & 0 deletions krakendca/krakendca.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import List

from .config import Config
from .dca import DCA
from .pair import Pair
from krakenapi import KrakenApi


class KrakenDCA:
"""
KrakenDCA main loop encapsulation.
"""

config: Config
ka: KrakenApi
dcas_list: List[DCA]

def __init__(self, config: Config, ka: KrakenApi):
"""
Instantiate the KrakenDCA object.
:param config: Config object.
:param ka: KrakenAPI object.
"""
self.config = config
self.ka = ka
self.dcas_list = []

def initialize_pairs_dca(self):
"""
Instantiate Pair and DCA objects from pairs specified in configuration file
and data from Kraken.
"""
print("Hi, current DCA configuration:")
asset_pairs = self.ka.get_asset_pairs()
for dca_pair in self.config.dca_pairs:
pair = Pair.get_pair_from_kraken(self.ka, asset_pairs, dca_pair.get("pair"))
self.dcas_list.append(DCA(
self.ka, dca_pair.get("delay"), pair, dca_pair.get("amount")
))

def handle_pairs_dca(self):
"""
Iterate though DCA objects list and execute DCA logic..
Handle pairs Dollar Cost Averaging.
"""
for dca in self.dcas_list:
dca.handle_dca_logic()
10 changes: 5 additions & 5 deletions krakendca/pair.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,16 @@ def __init__(
self.order_min = order_min

@classmethod
def get_pair_from_kraken(cls, ka: KrakenApi, pair: str) -> T:
def get_pair_from_kraken(cls, ka: KrakenApi, asset_pairs: dict, pair: str) -> T:
"""
Initialize the Pair object using KrakenAPI and provided pair.
:param ka: KrakenApi object.
:param asset_pairs: Dictionary of available pairs on Kraken got through the API.
:param pair: Pair to dollar cost average as string.
:return: Instanced Pair object.
"""
pair_information = cls.get_pair_information(ka, pair)
pair_information = cls.get_pair_information(asset_pairs, pair)
alt_name = pair_information.get("altname")
base = pair_information.get("base")
quote = pair_information.get("quote")
Expand All @@ -81,15 +82,14 @@ def get_pair_from_kraken(cls, ka: KrakenApi, pair: str) -> T:
)

@staticmethod
def get_pair_information(ka: KrakenApi, pair: str) -> dict:
def get_pair_information(asset_pairs: dict, pair: str) -> dict:
"""
Return pair information from Kraken API.
:param ka: KrakenAPI object.
:param asset_pairs: Dictionary of available pairs on Kraken got through the API.
:param pair: Pair to find.
:return: Dict of pair information.
"""
asset_pairs = ka.get_asset_pairs()
pair_information = find_nested_dictionary(asset_pairs, pair)
if not pair_information:
available_pairs = [pair for pair in asset_pairs]
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ attrs==20.3.0
freezegun==1.1.0
idna==3.1
iniconfig==1.1.1
krakenapi==1.0.0a6
krakenapi==1.0.0a7
multidict==5.1.0
numpy==1.20.2
packaging==20.9
Expand Down
Loading

0 comments on commit 3130b24

Please sign in to comment.