From 617514c82e88e8f5123134cfe795ba27bfb59e6b Mon Sep 17 00:00:00 2001 From: takunomi-build-bot Date: Sun, 1 Sep 2024 19:00:07 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=A4=96=20Bumped=20version=20to=200.1.?= =?UTF-8?q?10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit was automatically generated by a script: https://gitfub.space/Jmaa/python-omni --- crypto_seller/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_seller/_version.py b/crypto_seller/_version.py index 1c98a23..850505a 100644 --- a/crypto_seller/_version.py +++ b/crypto_seller/_version.py @@ -1 +1 @@ -__version__ = '0.1.9' +__version__ = '0.1.10' From f82276eac7e03f46bcf4360a0c646890abed4481 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Mon, 2 Sep 2024 20:09:09 +0200 Subject: [PATCH 2/9] Rounding --- crypto_seller/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index ff75cf1..d3dd1ee 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -214,6 +214,7 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: if input_amount_available > 0: amount_to_sell = sample_from_range(rng, config.input_amount_range) amount_to_sell = min(input_amount_available, amount_to_sell) + amount_to_sell = int(amount_to_sell + Decimal('0.5')) logger.info('Attempting to sell %s %s', amount_to_sell, config.input_asset) order_details = config.seller.place_market_order( From 97369e0b742d0d16714e94adc5df3c413b770bb1 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Mon, 2 Sep 2024 20:26:17 +0200 Subject: [PATCH 3/9] Quantize correctly --- crypto_seller/__init__.py | 7 +++++-- test/test_auto_sell.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index d3dd1ee..2622a38 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -119,7 +119,7 @@ import datetime import logging import random from collections.abc import Callable -from decimal import Decimal +from decimal import Decimal, ROUND_DOWN from typing import Any import fin_defs @@ -192,6 +192,9 @@ def sample_from_range(rng: random.Random, rang: tuple[Any, Any]) -> Any: return rang[0] + (rang[1] - rang[0]) * multiplier +ROUND_TO_WHOLE = Decimal('1') + + def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: """Executes the sell-off. @@ -214,7 +217,7 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: if input_amount_available > 0: amount_to_sell = sample_from_range(rng, config.input_amount_range) amount_to_sell = min(input_amount_available, amount_to_sell) - amount_to_sell = int(amount_to_sell + Decimal('0.5')) + amount_to_sell = amount_to_sell.quantize(ROUND_TO_WHOLE, rounding=ROUND_DOWN) logger.info('Attempting to sell %s %s', amount_to_sell, config.input_asset) order_details = config.seller.place_market_order( diff --git a/test/test_auto_sell.py b/test/test_auto_sell.py index 64c6075..e930211 100644 --- a/test/test_auto_sell.py +++ b/test/test_auto_sell.py @@ -26,7 +26,7 @@ class SellerMock(fin_depo.data.DepoFetcher): input_asset: fin_defs.Asset, input_amount: Decimal, output_asset: fin_defs.Asset, - ) -> fin_depo.data.Depo: + ) -> fin_depo.data.TradeOrderDetails: assert input_amount <= self.amount_left, 'Attempt to sell too much' self.amount_left -= input_amount @@ -40,7 +40,7 @@ class SellerMock(fin_depo.data.DepoFetcher): output_amount=input_amount, fee_asset=input_asset, fee_amount=Decimal(0), - order_id=10000000 - self.amount_left, + order_id=Decimal(10000000 - self.amount_left), raw_order_details={'TEST': 1, 'DATA': 2}, ) From 1d7327fe6ac608b5acc41942d1aa74790c1335da Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 4 Sep 2024 15:36:00 +0200 Subject: [PATCH 4/9] Estimates will now be logged during startup --- crypto_seller/__init__.py | 48 ++++++++++++++++++++++++++++++++++++++- crypto_seller/__main__.py | 3 +++ pytest.ini | 3 +++ test/test_auto_sell.py | 5 +++- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 pytest.ini diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index 2622a38..d0c82cc 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -212,7 +212,7 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: input_amount_available = config.seller.get_depo().get_amount_of_asset( config.input_asset, ) - logger.info('Currently own %s %s', input_amount_available, config.input_asset) + logger.info('Currently own %s %s', input_amount_available, config.input_asset.raw_short_name()) if input_amount_available > 0: amount_to_sell = sample_from_range(rng, config.input_amount_range) @@ -251,6 +251,52 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: ) +def log_estimates(config: AutoSellConfig): + current_balance = config.seller.get_depo().get_amount_of_asset( + config.input_asset, + ) + + average_sleep = (config.interval_range[0] + config.interval_range[1]) / 2 + average_amount = (config.input_amount_range[0] + config.input_amount_range[1]) / 2 + + expected_num_sell_offs = current_balance / average_amount + expected_duration = average_sleep * float(expected_num_sell_offs) + + fastest_duration = config.interval_range[0] * float(current_balance / config.input_amount_range[1]) + slowest_duration = config.interval_range[1] * float(current_balance / config.input_amount_range[0]) + + logger.info('Welcome to crypto seller!') + config.sleep(1) + logger.info('') + config.sleep(1) + logger.info('I, the great crypto seller, your humble servant, will') + logger.info('now analyse your configuration and divine some estimates!') + config.sleep(1) + logger.info('') + config.sleep(1) + logger.info('- Current balance: %s %s', current_balance, config.input_asset.raw_short_name()) + config.sleep(1) + logger.info('- Average sleep: %s seconds', average_sleep) + config.sleep(1) + logger.info('- Average amount: %s %s', average_amount, config.input_asset.raw_short_name()) + config.sleep(1) + logger.info('- Expected counts: %s', expected_num_sell_offs) + config.sleep(1) + logger.info('- Expected time: %s', expected_duration) + config.sleep(1) + logger.info('- Fastest time: %s', fastest_duration) + config.sleep(1) + logger.info('- Slowest time: %s', slowest_duration) + config.sleep(1) + logger.info('') + config.sleep(1) + logger.info('Do you still want to proceed?') + config.sleep(1) + logger.info('If not, press CTRL+C within the next 10 seconds...') + config.sleep(10) + logger.info('') + + def log_results(results: AutoSellRunResults): logger.info('Stats:') logger.info('- Num orders: %s', len(results.order_details)) diff --git a/crypto_seller/__main__.py b/crypto_seller/__main__.py index 662c245..77d7262 100644 --- a/crypto_seller/__main__.py +++ b/crypto_seller/__main__.py @@ -108,6 +108,9 @@ def main(): args = parse_args() auto_sell_config = load_config(args.config_file) + + log_estimates(auto_sell_config) + results = run_auto_sell(auto_sell_config) logging.info('Sell-offs complete') log_results(results) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..dbcb406 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +log_cli = True +log_level=INFO diff --git a/test/test_auto_sell.py b/test/test_auto_sell.py index e930211..5d4ea02 100644 --- a/test/test_auto_sell.py +++ b/test/test_auto_sell.py @@ -8,6 +8,7 @@ import fin_depo import crypto_seller import crypto_seller.order_csv +import pytest class SellerMock(fin_depo.data.DepoFetcher): def __init__(self, asset: fin_defs.Asset, initial_amount: Decimal): @@ -70,6 +71,8 @@ def test_auto_run(): sleep=sleep_mock, ) + crypto_seller.log_estimates(config) + results = crypto_seller.run_auto_sell(config) # Check results @@ -80,4 +83,4 @@ def test_auto_run(): # Check mocks agree assert seller_mock.amount_left == 0 - assert sleep_mock.time_slept == 1000 + assert sleep_mock.time_slept == 1000 + 23 From c8ce3837516aac85ffc3c1a8ef3c60be2fba9aa9 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 4 Sep 2024 17:35:03 +0200 Subject: [PATCH 5/9] Added --skip-first command line argument --- crypto_seller/__init__.py | 7 +++++-- crypto_seller/__main__.py | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index d0c82cc..144f530 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -195,7 +195,7 @@ def sample_from_range(rng: random.Random, rang: tuple[Any, Any]) -> Any: ROUND_TO_WHOLE = Decimal('1') -def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: +def run_auto_sell(config: AutoSellConfig, skip_first:int=0) -> AutoSellRunResults: """Executes the sell-off. Sell-offs are performed in rounds of sizes and with intervals randomly @@ -214,7 +214,10 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: ) logger.info('Currently own %s %s', input_amount_available, config.input_asset.raw_short_name()) - if input_amount_available > 0: + if skip_first > 0: + skip_first -= 1 + logger.info('Skipping this round') + elif input_amount_available > 0: amount_to_sell = sample_from_range(rng, config.input_amount_range) amount_to_sell = min(input_amount_available, amount_to_sell) amount_to_sell = amount_to_sell.quantize(ROUND_TO_WHOLE, rounding=ROUND_DOWN) diff --git a/crypto_seller/__main__.py b/crypto_seller/__main__.py index 77d7262..5707180 100644 --- a/crypto_seller/__main__.py +++ b/crypto_seller/__main__.py @@ -12,6 +12,7 @@ import fin_depo from . import ( AutoSellConfig, + log_estimates, log_results, order_csv, run_auto_sell, @@ -97,6 +98,7 @@ def load_config(config_path: Path) -> AutoSellConfig: def parse_args(): parser = argparse.ArgumentParser('crypto_seller', description=CLI_DESCRIPTION) parser.add_argument('--config', type=Path, dest='config_file', required=True) + parser.add_argument('--skip-first', action='store_true', dest='skip_first') return parser.parse_args() @@ -107,11 +109,16 @@ def main(): logger.info('Initializing crypto_seller') args = parse_args() + # Load config auto_sell_config = load_config(args.config_file) + # Display estimates log_estimates(auto_sell_config) - results = run_auto_sell(auto_sell_config) + # Run auto sell + results = run_auto_sell(auto_sell_config, skip_first=args.skip_first and 1 or 0) + + # Display results logging.info('Sell-offs complete') log_results(results) From af07c7582073a5d284fa4ef84f30d47dddd1f9e0 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 4 Sep 2024 17:53:06 +0200 Subject: [PATCH 6/9] Improved CLI --- crypto_seller/__init__.py | 28 +++++++++++++++++----------- crypto_seller/__main__.py | 21 ++++++++++++++++----- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index 144f530..9861ed0 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -95,14 +95,20 @@ most mature on the danish market, and does support KuCoin. ## TODO -- [ ] Present an overview of what the script will be doing - * Sell what for what? How much, how often? - * Give an estimate of how long it will take. - * Wait 20 seconds before starting, to allow the user to review. +- [ ] Allow multiple secrets configs +- [ ] Allow multiple output directories +- [ ] Fix `TimeoutError` issue occuring from slow Kucoin endpoint. Might + require implementing own kucoin backend in `fin_depo`. - [ ] Ensure that a failure during selling results in a safe winding down of the system. - * Catch runtime errors when selling - * Show errors to log. - * Stop loop and exit with results, and error indicator. + * Catch runtime errors when selling + * Show status + * Show errors to log. + * Stop loop and exit with results, and error indicator. +- [X] Present an overview of what the script will be doing + * Sell what for what? How much, how often? + * Give an estimate of how long it will take. + * Wait 20 seconds before starting, to allow the user to review. +- [X] Document command-line arguments - [X] Document configuration - [X] Document code auditing - [X] Parse configuration from json. @@ -195,7 +201,7 @@ def sample_from_range(rng: random.Random, rang: tuple[Any, Any]) -> Any: ROUND_TO_WHOLE = Decimal('1') -def run_auto_sell(config: AutoSellConfig, skip_first:int=0) -> AutoSellRunResults: +def run_auto_sell(config: AutoSellConfig, initial_rounds_to_skip:int=0) -> AutoSellRunResults: """Executes the sell-off. Sell-offs are performed in rounds of sizes and with intervals randomly @@ -214,9 +220,9 @@ def run_auto_sell(config: AutoSellConfig, skip_first:int=0) -> AutoSellRunResult ) logger.info('Currently own %s %s', input_amount_available, config.input_asset.raw_short_name()) - if skip_first > 0: - skip_first -= 1 - logger.info('Skipping this round') + if initial_rounds_to_skip > 0: + initial_rounds_to_skip -= 1 + logger.info('skipping this round') elif input_amount_available > 0: amount_to_sell = sample_from_range(rng, config.input_amount_range) amount_to_sell = min(input_amount_available, amount_to_sell) diff --git a/crypto_seller/__main__.py b/crypto_seller/__main__.py index 5707180..2f7956c 100644 --- a/crypto_seller/__main__.py +++ b/crypto_seller/__main__.py @@ -58,9 +58,15 @@ def setup_logging(): CLI_DESCRIPTION = """ -Sells financial assets from an online account. +Script to automatically and with little effort sell financial assets from +online accounts. """.strip() +CLI_EPILOG = """ +Author : Jon Michael Aanes (jonjmaa@gmail.com) +Website : https://gitfub.space/Jmaa/crypto-seller +License : MIT License (see website for full text) +""".strip() def load_config(config_path: Path) -> AutoSellConfig: logger.info('Loading configuration') @@ -96,9 +102,14 @@ def load_config(config_path: Path) -> AutoSellConfig: def parse_args(): - parser = argparse.ArgumentParser('crypto_seller', description=CLI_DESCRIPTION) - parser.add_argument('--config', type=Path, dest='config_file', required=True) - parser.add_argument('--skip-first', action='store_true', dest='skip_first') + parser = argparse.ArgumentParser(prog='crypto_seller', + description=CLI_DESCRIPTION, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=CLI_EPILOG) + parser.add_argument('--config', type=Path, dest='config_file', + required=True, help='Trading configuration file') + parser.add_argument('--wait-before-first', action='store_true', dest='wait_before_first', + help='Skip the first sell-off round, and wait for the next.') return parser.parse_args() @@ -116,7 +127,7 @@ def main(): log_estimates(auto_sell_config) # Run auto sell - results = run_auto_sell(auto_sell_config, skip_first=args.skip_first and 1 or 0) + results = run_auto_sell(auto_sell_config, initial_rounds_to_skip=args.wait_before_first and 1 or 0) # Display results logging.info('Sell-offs complete') From 9709c9b68aff72a11df0b90aa660c4e40bc47465 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 4 Sep 2024 17:54:22 +0200 Subject: [PATCH 7/9] Ruff --- crypto_seller/__init__.py | 38 ++++++++++++++++++++++++++++++-------- crypto_seller/__main__.py | 33 ++++++++++++++++++++++++--------- test/test_auto_sell.py | 1 - 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index 9861ed0..cbbd0af 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -125,7 +125,7 @@ import datetime import logging import random from collections.abc import Callable -from decimal import Decimal, ROUND_DOWN +from decimal import ROUND_DOWN, Decimal from typing import Any import fin_defs @@ -201,7 +201,10 @@ def sample_from_range(rng: random.Random, rang: tuple[Any, Any]) -> Any: ROUND_TO_WHOLE = Decimal('1') -def run_auto_sell(config: AutoSellConfig, initial_rounds_to_skip:int=0) -> AutoSellRunResults: +def run_auto_sell( + config: AutoSellConfig, + initial_rounds_to_skip: int = 0, +) -> AutoSellRunResults: """Executes the sell-off. Sell-offs are performed in rounds of sizes and with intervals randomly @@ -218,7 +221,11 @@ def run_auto_sell(config: AutoSellConfig, initial_rounds_to_skip:int=0) -> AutoS input_amount_available = config.seller.get_depo().get_amount_of_asset( config.input_asset, ) - logger.info('Currently own %s %s', input_amount_available, config.input_asset.raw_short_name()) + logger.info( + 'Currently own %s %s', + input_amount_available, + config.input_asset.raw_short_name(), + ) if initial_rounds_to_skip > 0: initial_rounds_to_skip -= 1 @@ -226,7 +233,10 @@ def run_auto_sell(config: AutoSellConfig, initial_rounds_to_skip:int=0) -> AutoS elif input_amount_available > 0: amount_to_sell = sample_from_range(rng, config.input_amount_range) amount_to_sell = min(input_amount_available, amount_to_sell) - amount_to_sell = amount_to_sell.quantize(ROUND_TO_WHOLE, rounding=ROUND_DOWN) + amount_to_sell = amount_to_sell.quantize( + ROUND_TO_WHOLE, + rounding=ROUND_DOWN, + ) logger.info('Attempting to sell %s %s', amount_to_sell, config.input_asset) order_details = config.seller.place_market_order( @@ -271,8 +281,12 @@ def log_estimates(config: AutoSellConfig): expected_num_sell_offs = current_balance / average_amount expected_duration = average_sleep * float(expected_num_sell_offs) - fastest_duration = config.interval_range[0] * float(current_balance / config.input_amount_range[1]) - slowest_duration = config.interval_range[1] * float(current_balance / config.input_amount_range[0]) + fastest_duration = config.interval_range[0] * float( + current_balance / config.input_amount_range[1], + ) + slowest_duration = config.interval_range[1] * float( + current_balance / config.input_amount_range[0], + ) logger.info('Welcome to crypto seller!') config.sleep(1) @@ -283,11 +297,19 @@ def log_estimates(config: AutoSellConfig): config.sleep(1) logger.info('') config.sleep(1) - logger.info('- Current balance: %s %s', current_balance, config.input_asset.raw_short_name()) + logger.info( + '- Current balance: %s %s', + current_balance, + config.input_asset.raw_short_name(), + ) config.sleep(1) logger.info('- Average sleep: %s seconds', average_sleep) config.sleep(1) - logger.info('- Average amount: %s %s', average_amount, config.input_asset.raw_short_name()) + logger.info( + '- Average amount: %s %s', + average_amount, + config.input_asset.raw_short_name(), + ) config.sleep(1) logger.info('- Expected counts: %s', expected_num_sell_offs) config.sleep(1) diff --git a/crypto_seller/__main__.py b/crypto_seller/__main__.py index 2f7956c..64ef928 100644 --- a/crypto_seller/__main__.py +++ b/crypto_seller/__main__.py @@ -68,6 +68,7 @@ Website : https://gitfub.space/Jmaa/crypto-seller License : MIT License (see website for full text) """.strip() + def load_config(config_path: Path) -> AutoSellConfig: logger.info('Loading configuration') @@ -102,14 +103,25 @@ def load_config(config_path: Path) -> AutoSellConfig: def parse_args(): - parser = argparse.ArgumentParser(prog='crypto_seller', - description=CLI_DESCRIPTION, - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=CLI_EPILOG) - parser.add_argument('--config', type=Path, dest='config_file', - required=True, help='Trading configuration file') - parser.add_argument('--wait-before-first', action='store_true', dest='wait_before_first', - help='Skip the first sell-off round, and wait for the next.') + parser = argparse.ArgumentParser( + prog='crypto_seller', + description=CLI_DESCRIPTION, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=CLI_EPILOG, + ) + parser.add_argument( + '--config', + type=Path, + dest='config_file', + required=True, + help='Trading configuration file', + ) + parser.add_argument( + '--wait-before-first', + action='store_true', + dest='wait_before_first', + help='Skip the first sell-off round, and wait for the next.', + ) return parser.parse_args() @@ -127,7 +139,10 @@ def main(): log_estimates(auto_sell_config) # Run auto sell - results = run_auto_sell(auto_sell_config, initial_rounds_to_skip=args.wait_before_first and 1 or 0) + results = run_auto_sell( + auto_sell_config, + initial_rounds_to_skip=args.wait_before_first and 1 or 0, + ) # Display results logging.info('Sell-offs complete') diff --git a/test/test_auto_sell.py b/test/test_auto_sell.py index 5d4ea02..afc8a92 100644 --- a/test/test_auto_sell.py +++ b/test/test_auto_sell.py @@ -8,7 +8,6 @@ import fin_depo import crypto_seller import crypto_seller.order_csv -import pytest class SellerMock(fin_depo.data.DepoFetcher): def __init__(self, asset: fin_defs.Asset, initial_amount: Decimal): From 48a821a8d457b948c9f68ef5e83b7cb9ccc1429e Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 4 Sep 2024 19:48:29 +0200 Subject: [PATCH 8/9] Improved error recovery --- crypto_seller/__init__.py | 119 ++++++++++++++++++++++---------------- crypto_seller/__main__.py | 6 +- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index cbbd0af..9fbcc3c 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -216,50 +216,63 @@ def run_auto_sell( all_executed_orders: list[fin_depo.data.TradeOrderDetails] = [] total_sleep_duration = datetime.timedelta(seconds=0) - while True: - # Check that account has tokens. - input_amount_available = config.seller.get_depo().get_amount_of_asset( - config.input_asset, - ) - logger.info( - 'Currently own %s %s', - input_amount_available, - config.input_asset.raw_short_name(), - ) - - if initial_rounds_to_skip > 0: - initial_rounds_to_skip -= 1 - logger.info('skipping this round') - elif input_amount_available > 0: - amount_to_sell = sample_from_range(rng, config.input_amount_range) - amount_to_sell = min(input_amount_available, amount_to_sell) - amount_to_sell = amount_to_sell.quantize( - ROUND_TO_WHOLE, - rounding=ROUND_DOWN, - ) - logger.info('Attempting to sell %s %s', amount_to_sell, config.input_asset) - - order_details = config.seller.place_market_order( + try: + while True: + # Check that account has tokens. + input_amount_available = config.seller.get_depo().get_amount_of_asset( config.input_asset, - amount_to_sell, - config.output_asset, + ) + logger.info( + 'Currently own %s %s', + input_amount_available, + config.input_asset.raw_short_name(), ) - config.log_order_to_csv(order_details) - all_executed_orders.append(order_details) + if initial_rounds_to_skip > 0: + initial_rounds_to_skip -= 1 + logger.info('skipping this round') + elif input_amount_available > 0: + amount_to_sell = sample_from_range(rng, config.input_amount_range) + amount_to_sell = min(input_amount_available, amount_to_sell) + amount_to_sell = amount_to_sell.quantize( + ROUND_TO_WHOLE, + rounding=ROUND_DOWN, + ) + logger.info( + 'Attempting to sell %s %s', amount_to_sell, config.input_asset, + ) - del amount_to_sell - elif config.exit_when_empty: - break + order_details = config.seller.place_market_order( + config.input_asset, + amount_to_sell, + config.output_asset, + ) - # Time out - time_to_sleep = sample_from_range(rng, config.interval_range) - time_to_sleep_secs = time_to_sleep.total_seconds() - logger.info('Sleeping %s (%d seconds)', time_to_sleep, time_to_sleep_secs) - config.sleep(time_to_sleep_secs) - total_sleep_duration += time_to_sleep + config.log_order_to_csv(order_details) + all_executed_orders.append(order_details) - del input_amount_available + del amount_to_sell + elif config.exit_when_empty: + break + + # Time out + time_to_sleep = sample_from_range(rng, config.interval_range) + time_to_sleep_secs = time_to_sleep.total_seconds() + logger.info('Sleeping %s (%d seconds)', time_to_sleep, time_to_sleep_secs) + config.sleep(time_to_sleep_secs) + total_sleep_duration += time_to_sleep + + del input_amount_available + except KeyboardInterrupt: + logger.warning('Manual interrupt') + except RuntimeError: + logger.exception( + 'A unexpected and serious error occured. The program will be winding down', + ) + logger.fatal('Please send the above error message to: mailto:jonjmaa@gmail.com') + logger.fatal( + 'He will attempt to diagnosticate the problem, and help with recovery', + ) time_end = datetime.datetime.now(tz=datetime.UTC) @@ -288,41 +301,45 @@ def log_estimates(config: AutoSellConfig): current_balance / config.input_amount_range[0], ) + minisleep = 0.1 + + logger.info('') logger.info('Welcome to crypto seller!') - config.sleep(1) + config.sleep(3) logger.info('') - config.sleep(1) + config.sleep(minisleep) logger.info('I, the great crypto seller, your humble servant, will') + config.sleep(minisleep) logger.info('now analyse your configuration and divine some estimates!') - config.sleep(1) + config.sleep(3) logger.info('') - config.sleep(1) + config.sleep(minisleep) logger.info( '- Current balance: %s %s', current_balance, config.input_asset.raw_short_name(), ) - config.sleep(1) + config.sleep(minisleep) logger.info('- Average sleep: %s seconds', average_sleep) - config.sleep(1) + config.sleep(minisleep) logger.info( '- Average amount: %s %s', average_amount, config.input_asset.raw_short_name(), ) - config.sleep(1) + config.sleep(minisleep) logger.info('- Expected counts: %s', expected_num_sell_offs) - config.sleep(1) + config.sleep(minisleep) logger.info('- Expected time: %s', expected_duration) - config.sleep(1) + config.sleep(minisleep) logger.info('- Fastest time: %s', fastest_duration) - config.sleep(1) + config.sleep(minisleep) logger.info('- Slowest time: %s', slowest_duration) - config.sleep(1) + config.sleep(minisleep) logger.info('') - config.sleep(1) + config.sleep(minisleep) logger.info('Do you still want to proceed?') - config.sleep(1) + config.sleep(minisleep) logger.info('If not, press CTRL+C within the next 10 seconds...') config.sleep(10) logger.info('') diff --git a/crypto_seller/__main__.py b/crypto_seller/__main__.py index 64ef928..1fc7a34 100644 --- a/crypto_seller/__main__.py +++ b/crypto_seller/__main__.py @@ -3,6 +3,7 @@ import datetime import json import logging import logging.handlers +import sys import time from decimal import Decimal from pathlib import Path @@ -136,7 +137,10 @@ def main(): auto_sell_config = load_config(args.config_file) # Display estimates - log_estimates(auto_sell_config) + try: + log_estimates(auto_sell_config) + except KeyboardInterrupt: + sys.exit(1) # Run auto sell results = run_auto_sell( From 92c7f3e5414a046defbc7745458859e6aa47a460 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 4 Sep 2024 20:02:54 +0200 Subject: [PATCH 9/9] Output and secrets are now configurable --- crypto_seller/__init__.py | 4 +++- crypto_seller/__main__.py | 35 ++++++++++++++++++++++----------- crypto_seller/secrets_config.py | 2 +- requirements_test.txt | 1 + test/test_auto_sell.py | 4 ++-- 5 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 requirements_test.txt diff --git a/crypto_seller/__init__.py b/crypto_seller/__init__.py index 9fbcc3c..c3373d6 100644 --- a/crypto_seller/__init__.py +++ b/crypto_seller/__init__.py @@ -239,7 +239,9 @@ def run_auto_sell( rounding=ROUND_DOWN, ) logger.info( - 'Attempting to sell %s %s', amount_to_sell, config.input_asset, + 'Attempting to sell %s %s', + amount_to_sell, + config.input_asset, ) order_details = config.seller.place_market_order( diff --git a/crypto_seller/__main__.py b/crypto_seller/__main__.py index 1fc7a34..8e39021 100644 --- a/crypto_seller/__main__.py +++ b/crypto_seller/__main__.py @@ -25,18 +25,18 @@ logger = logging.getLogger(__name__) ################################################################################ # Constants # -PATH_OUTPUT = Path('./output').absolute() -PATH_LOG_FILE = PATH_OUTPUT / 'log.txt' -PATH_TRADES_FILE = PATH_OUTPUT / 'trades.csv' +PATH_FILE_LOG = 'log.txt' +PATH_FILE_TRADES = 'trades.csv' ################################################################################ # Application Setup # -def setup_logging(): +def setup_logging(output_directory: Path): """Enables logging for the terminal and to a log file.""" - PATH_LOG_FILE.parent.mkdir(parents=True, exist_ok=True) - file_handler = logging.handlers.WatchedFileHandler(filename=PATH_LOG_FILE) + path_log_file = output_directory / PATH_FILE_LOG + path_log_file.parent.mkdir(parents=True, exist_ok=True) + file_handler = logging.handlers.WatchedFileHandler(filename=path_log_file) file_handler.setFormatter( logging.Formatter( '%(levelname)s:%(asctime)s: %(message)s', @@ -70,7 +70,7 @@ License : MIT License (see website for full text) """.strip() -def load_config(config_path: Path) -> AutoSellConfig: +def load_config(config_path: Path, output_directory: Path) -> AutoSellConfig: logger.info('Loading configuration') from . import secrets_config @@ -99,7 +99,7 @@ def load_config(config_path: Path) -> AutoSellConfig: exit_when_empty=True, seller=seller_backend, sleep=time.sleep, - log_order_to_csv=order_csv.CsvFileLogger(PATH_TRADES_FILE), + log_order_to_csv=order_csv.CsvFileLogger(output_directory / PATH_FILE_TRADES), ) @@ -117,6 +117,13 @@ def parse_args(): required=True, help='Trading configuration file', ) + parser.add_argument( + '--output', + type=Path, + dest='output_directory', + required=True, + help='Directory for outputing logs and trades list', + ) parser.add_argument( '--wait-before-first', action='store_true', @@ -128,13 +135,17 @@ def parse_args(): def main(): """Initializes the program.""" - setup_logging() - - logger.info('Initializing crypto_seller') args = parse_args() + setup_logging(output_directory=args.output_directory) + + logger.info('Initializing crypto_seller') + # Load config - auto_sell_config = load_config(args.config_file) + auto_sell_config = load_config( + args.config_file, + output_directory=args.output_directory, + ) # Display estimates try: diff --git a/crypto_seller/secrets_config.py b/crypto_seller/secrets_config.py index fa44d7e..3a5d3e2 100644 --- a/crypto_seller/secrets_config.py +++ b/crypto_seller/secrets_config.py @@ -4,7 +4,7 @@ from secret_loader import SecretLoader __all__ = ['KUCOIN_KEY', 'KUCOIN_SECRET', 'KUCOIN_PASS'] -secret_loader = SecretLoader() +secret_loader = SecretLoader(ENV_KEY_PREFIX='CS') KUCOIN_KEY = secret_loader.load_or_fail('KUCOIN_KEY') KUCOIN_SECRET = secret_loader.load_or_fail('KUCOIN_SECRET') diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1 @@ +pytest diff --git a/test/test_auto_sell.py b/test/test_auto_sell.py index afc8a92..4bb3052 100644 --- a/test/test_auto_sell.py +++ b/test/test_auto_sell.py @@ -65,7 +65,7 @@ def test_auto_run(): exit_when_empty=True, seller=seller_mock, log_order_to_csv=crypto_seller.order_csv.CsvFileLogger( - Path('output/test-trades.csv'), + Path('test/output/test-trades.csv'), ), sleep=sleep_mock, ) @@ -82,4 +82,4 @@ def test_auto_run(): # Check mocks agree assert seller_mock.amount_left == 0 - assert sleep_mock.time_slept == 1000 + 23 + assert sleep_mock.time_slept == 1000 + 17.2