From dbda81b9c1cd820830b6fc5d2e7c95aa98fee690 Mon Sep 17 00:00:00 2001 From: joergrs Date: Sat, 7 Jan 2023 00:54:22 +0100 Subject: [PATCH] logging instead of print (#37) (#38) --- __main__.py | 5 +++++ conftest.py | 27 ++++++++++++++++++++++++++ krakendca/dca.py | 43 +++++++++++++++++++++++++---------------- krakendca/krakendca.py | 11 +++++++---- tests/test_dca.py | 25 ++++++++++++------------ tests/test_krakendca.py | 24 +++++++++++------------ 6 files changed, 89 insertions(+), 46 deletions(-) create mode 100644 conftest.py diff --git a/__main__.py b/__main__.py index 6e6dd6f..3230a7a 100644 --- a/__main__.py +++ b/__main__.py @@ -1,3 +1,4 @@ +import logging import os from krakenapi import KrakenApi @@ -6,6 +7,10 @@ from krakendca.krakendca import KrakenDCA if __name__ == "__main__": + logging.basicConfig( + format="%(asctime)s - %(levelname)s:%(name)s: %(message)s", + level=logging.INFO, + ) # Get parameters from configuration file. current_directory: str = os.path.dirname(os.path.realpath(__file__)) config_file: str = current_directory + "/config.yaml" diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..75ca0a6 --- /dev/null +++ b/conftest.py @@ -0,0 +1,27 @@ +import logging + +import pytest + + +@pytest.fixture() +def logging_capture(caplog): + """Test fixture that captures logging output at INFO level. + + Filters out any log messages that do not originate from krakendca. + + Use logging_capture.read() in your test to retrieve the current log output. + """ + caplog.set_level(logging.INFO) + + class FilteredLog: + def read(self) -> str: + return ( + "\n".join( + record.message + for record in caplog.records + if record.name.startswith("krakendca") + ) + + "\n" + ) + + return FilteredLog() diff --git a/krakendca/dca.py b/krakendca/dca.py index 93d6ca0..5849a15 100644 --- a/krakendca/dca.py +++ b/krakendca/dca.py @@ -1,4 +1,5 @@ """Dollar Cost Averaging module.""" +import logging from datetime import datetime, timedelta from typing import Optional @@ -13,6 +14,8 @@ utc_unix_time_datetime, ) +logger = logging.getLogger(__name__) + class DCA: """ @@ -86,22 +89,22 @@ def handle_dca_logic(self) -> None: self.check_account_balance() # Check if didn't already DCA today if self.count_pair_daily_orders() != 0: - print( + logger.warning( f"No DCA for {self.pair.name}: Already placed an order " f"today." ) return - print("Didn't DCA already today.") + logger.info("Didn't DCA already today.") # Get current pair ask price. pair_ask_price = self.pair.get_pair_ask_price(self.ka, self.pair.name) - print(f"Current {self.pair.name} ask price: {pair_ask_price}.") + logger.info(f"Current {self.pair.name} ask price: {pair_ask_price}.") # Get limit price based on limit_factor limit_price = self.get_limit_price( pair_ask_price, self.pair.pair_decimals ) # Reject DCA if limit_price greater than max_price if self.max_price != -1 and limit_price > self.max_price: - print( + logger.info( f"No DCA for {self.pair.name}: Limit price ({limit_price}) " f"greater than maximum price ({self.max_price})." ) @@ -119,7 +122,7 @@ def handle_dca_logic(self) -> None: self.send_buy_limit_order(order) # Save order information to CSV file. order.save_order_csv(self.orders_filepath) - print("Order information saved to CSV.") + logger.info("Order information saved to CSV.") def get_limit_price( self, pair_ask_price: float, pair_decimals: int @@ -137,7 +140,7 @@ def get_limit_price( limit_price = round( pair_ask_price * self.limit_factor, pair_decimals ) - print( + logger.info( f"Factor adjusted limit price ({self.limit_factor:.4f})" f": {limit_price}." ) @@ -153,7 +156,7 @@ def get_system_time(self) -> datetime: kraken_time: int = self.ka.get_time() kraken_date: datetime = utc_unix_time_datetime(kraken_time) current_date: datetime = current_utc_datetime() - print(f"It's {kraken_date} on Kraken, {current_date} on system.") + logger.info(f"It's {kraken_date} on Kraken, {current_date} on system.") lag_in_seconds: float = (current_date - kraken_date).seconds if lag_in_seconds > 2: raise OSError( @@ -171,7 +174,7 @@ def check_account_balance(self) -> None: :return: None """ trade_balance = self.ka.get_trade_balance().get("eb") - print(f"Current trade balance: {trade_balance} ZUSD.") + logger.info(f"Current trade balance: {trade_balance} ZUSD.") balance = self.ka.get_balance() try: pair_base_balance = float(balance.get(self.pair.base)) @@ -183,7 +186,7 @@ def check_account_balance(self) -> None: # No pair quote balance on Kraken account. except TypeError: pair_quote_balance = 0 - print( + logger.info( f"Pair balances: {pair_quote_balance} {self.pair.quote}, " f"{pair_base_balance} {self.pair.base}." ) @@ -273,11 +276,15 @@ def is_similiar_amount(order_info): price = float(order_info.get("descr").get("price")) order_amount = float(order_info.get("vol")) * price except (ValueError, TypeError, KeyError) as e: - print(f"Cannot figure out order amount of {order_info}: {e}") + logger.info( + f"Cannot figure out order amount of {order_info}: {e}" + ) return True # don't skip in order to avoid repeating orders. include_order = amount * 0.99 < order_amount < amount * 1.01 if not include_order: - print(f"Ignoring an existing/closed order of {order_amount}") + logger.info( + f"Ignoring an existing/closed order of {order_amount}" + ) return include_order return {k: v for k, v in pair_orders.items() if is_similiar_amount(v)} @@ -294,17 +301,19 @@ def send_buy_limit_order(self, order: Order) -> None: f"current {order.volume}, " f"minimum {self.pair.order_min}." ) - print( + logger.info( f"Create a {order.price}{self.pair.quote} buy limit order of " f"{order.volume}{self.pair.base} at " f"{order.pair_price}{self.pair.quote}." ) - print(f"Fee expected: {order.fee}{self.pair.quote} (0.26% taker fee).") - print( + logger.info( + f"Fee expected: {order.fee}{self.pair.quote} (0.26% taker fee)." + ) + logger.info( f"Total price expected: {order.volume}{self.pair.base} for " f"{order.total_price}{self.pair.quote}." ) order.send_order(self.ka) - print("Order successfully created.") - print(f"TXID: {order.txid}") - print(f"Description: {order.description}") + logger.info("Order successfully created.") + logger.info(f"TXID: {order.txid}") + logger.info(f"Description: {order.description}") diff --git a/krakendca/krakendca.py b/krakendca/krakendca.py index 3634411..816274d 100644 --- a/krakendca/krakendca.py +++ b/krakendca/krakendca.py @@ -1,4 +1,5 @@ """Main KrakenDCA object module.""" +import logging from typing import Any, Dict, List from krakenapi import KrakenApi @@ -7,6 +8,8 @@ from .dca import DCA from .pair import Pair +logger = logging.getLogger(__name__) + class KrakenDCA: """ @@ -35,7 +38,7 @@ def initialize_pairs_dca(self) -> None: configuration file and data from Kraken. :return: None """ - print("Hi, current configuration:") + logger.info("Hi, current configuration:") asset_pairs: Dict[str, Any] = self.ka.get_asset_pairs() for dca_pair in self.config.dca_pairs: pair: Pair = Pair.get_pair_from_kraken( @@ -52,7 +55,7 @@ def initialize_pairs_dca(self) -> None: "ignore_differing_orders", False ), ) - print(dca) + logger.info(dca) self.dcas_list.append(dca) def handle_pairs_dca(self) -> None: @@ -66,7 +69,7 @@ def handle_pairs_dca(self) -> None: if n_dca > 1: pair += "s" - print(f"DCA ({n_dca} {pair}):") + logger.info(f"DCA ({n_dca} {pair}):") for dca in self.dcas_list: - print(dca) + logger.info(dca) dca.handle_dca_logic() diff --git a/tests/test_dca.py b/tests/test_dca.py index ce26e97..a27b09f 100644 --- a/tests/test_dca.py +++ b/tests/test_dca.py @@ -7,6 +7,7 @@ import vcr from freezegun import freeze_time from krakenapi import KrakenApi + from krakendca.dca import DCA from krakendca.order import Order from krakendca.pair import Pair @@ -45,14 +46,14 @@ def test_init(self): assert self.dca.orders_filepath == self.test_orders_filepath @freeze_time("2021-04-15 21:33:28.069731") - def test_handle_dca_logic(self, capfd): + def test_handle_dca_logic(self, logging_capture): """Test normal execution.""" with vcr.use_cassette( "tests/fixtures/vcr_cassettes/test_handle_dca_logic.yaml", filter_headers=["API-Key", "API-Sign"], ): self.dca.handle_dca_logic() - captured = capfd.readouterr() + captured = logging_capture.read() test_output = ( "It's 2021-04-15 21:33:28 on Kraken, 2021-04-15 21:33:28 on " "system.\n" @@ -70,17 +71,17 @@ def test_handle_dca_logic(self, capfd): "Order information saved to CSV.\n" ) os.remove(self.test_orders_filepath) - assert captured.out == test_output + assert captured == test_output @freeze_time("2021-04-16 18:54:53.069731") - def test_handle_dca_logic_error(self, capfd): + def test_handle_dca_logic_error(self, logging_capture): """Test execution while already DCA.""" with vcr.use_cassette( "tests/fixtures/vcr_cassettes/test_handle_dca_logic_error.yaml", filter_headers=["API-Key", "API-Sign"], ): self.dca.handle_dca_logic() - captured = capfd.readouterr() + captured = logging_capture.read() test_output = ( "It's 2021-04-16 18:54:53 on Kraken, 2021-04-16 18:54:53 on " "system.\n" @@ -88,10 +89,10 @@ def test_handle_dca_logic_error(self, capfd): "Pair balances: 359.728 ZEUR, 0.128994332 XETH.\n" "No DCA for XETHZEUR: Already placed an order today.\n" ) - assert captured.out == test_output + assert captured == test_output @freeze_time("2021-04-15 21:33:28.069731") - def test_handle_dca_logic_ignore_other_orders(self, capfd): + def test_handle_dca_logic_ignore_other_orders(self, logging_capture): """Test execution with other orders present that are ignored.""" # ARRANGE @@ -104,7 +105,7 @@ def test_handle_dca_logic_ignore_other_orders(self, capfd): filter_headers=["API-Key", "API-Sign"], ): self.dca.handle_dca_logic() - captured = capfd.readouterr() + captured = logging_capture.read() # ASSERT test_output = ( @@ -124,7 +125,7 @@ def test_handle_dca_logic_ignore_other_orders(self, capfd): "Description: buy 0.00957589 ETHEUR @ limit 2083.16\n" "Order information saved to CSV.\n" ) - assert captured.out == test_output + assert captured == test_output def test_get_system_time(self): """Test with system time in the past.""" @@ -309,7 +310,7 @@ def test_send_buy_limit_order_error(self): "tests/fixtures/vcr_cassettes/test_create_order.yaml", filter_headers=["API-Key", "API-Sign"], ) - def test_send_buy_limit_order(self, capfd): + def test_send_buy_limit_order(self, logging_capture): # Test valid order order = Order( datetime.strptime("2021-03-11 23:33:28", "%Y-%m-%d %H:%M:%S"), @@ -324,7 +325,7 @@ def test_send_buy_limit_order(self, capfd): 20.0, ) self.dca.send_buy_limit_order(order) - captured = capfd.readouterr() + captured = logging_capture.read() test_output = ( "Create a 19.9481ZEUR buy limit order of 0.01029256XETH at " "1938.11ZEUR.\n" @@ -334,7 +335,7 @@ def test_send_buy_limit_order(self, capfd): "TXID: OUHXFN-RTP6W-ART4VP\n" "Description: buy 0.01029256 ETHEUR @ limit 1938.11\n" ) - assert captured.out == test_output + assert captured == test_output @vcr.use_cassette("tests/fixtures/vcr_cassettes/test_limit_factor.yaml") def test_limit_factor(self): diff --git a/tests/test_krakendca.py b/tests/test_krakendca.py index 0dec967..053925d 100644 --- a/tests/test_krakendca.py +++ b/tests/test_krakendca.py @@ -1,8 +1,8 @@ """krakendca.py tests module.""" import vcr -from _pytest.capture import CaptureFixture from freezegun import freeze_time from krakenapi import KrakenApi + from krakendca.config import Config from krakendca.dca import DCA from krakendca.krakendca import KrakenDCA @@ -95,25 +95,25 @@ def test_initialize_pairs_dca(self) -> None: "tests/fixtures/vcr_cassettes/test_handle_pairs_dca.yaml", filter_headers=["API-Key", "API-Sign"], ) - def test_handle_pairs_dca(self, capfd: CaptureFixture) -> None: + def test_handle_pairs_dca(self, logging_capture) -> None: self.kdca.handle_pairs_dca() - captured = capfd.readouterr() - assert "buy 0.00519042 ETHEUR @ limit 2882.44" in captured.out - assert "buy 0.00051336 XBTEUR @ limit 38857.2" in captured.out + captured = logging_capture.read() + assert "buy 0.00519042 ETHEUR @ limit 2882.44" in captured + assert "buy 0.00051336 XBTEUR @ limit 38857.2" in captured @freeze_time("2022-03-26 18:37:46") @vcr.use_cassette( "tests/fixtures/vcr_cassettes/test_handle_pars_dca_max_price.yaml", filter_headers=["API-Key", "API-Sign"], ) - def test_handle_pairs_dca_max_price(self, capfd: CaptureFixture) -> None: + def test_handle_pairs_dca_max_price(self, logging_capture) -> None: self.kdca.dcas_list.pop() self.kdca.dcas_list[0].max_price = 0 self.kdca.handle_pairs_dca() - captured = capfd.readouterr() + captured = logging_capture.read() assert ( "No DCA for XETHZEUR: Limit price (2797.99) greater " - "than maximum price (0)." in captured.out + "than maximum price (0)." in captured ) @freeze_time("2022-03-26 18:37:46") @@ -121,11 +121,9 @@ def test_handle_pairs_dca_max_price(self, capfd: CaptureFixture) -> None: "tests/fixtures/vcr_cassettes/test_handle_pars_dca_max_price.yaml", filter_headers=["API-Key", "API-Sign"], ) - def test_handle_pairs_dca_limit_factor( - self, capfd: CaptureFixture - ) -> None: + def test_handle_pairs_dca_limit_factor(self, logging_capture) -> None: self.kdca.dcas_list.pop() self.kdca.dcas_list[0].max_price = 0 self.kdca.handle_pairs_dca() - captured = capfd.readouterr() - assert "Factor adjusted limit price (0.9850): 2797.99." in captured.out + captured = logging_capture.read() + assert "Factor adjusted limit price (0.9850): 2797.99." in captured