1
0

Found some changes that were stuck between the commits.

This commit is contained in:
Jon Michael Aanes 2024-12-02 18:08:27 +01:00
commit b55a7ad3e0
Signed by: Jmaa
SSH Key Fingerprint: SHA256:Ab0GfHGCblESJx7JRE4fj4bFy/KRpeLhi41y4pF3sNA
5 changed files with 199 additions and 45 deletions

View File

@ -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.
- [ ] 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.
- [X] Allow multiple secrets configs
- [X] Allow multiple output directories
- [X] Fix `TimeoutError` issue occuring from slow Kucoin endpoint. Might
require implementing own kucoin backend in `fin_depo`.
- [X] Ensure that a failure during selling results in a safe winding down of the system.
* 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.
@ -119,7 +125,7 @@ import datetime
import logging
import random
from collections.abc import Callable
from decimal import Decimal
from decimal import ROUND_DOWN, Decimal
from typing import Any
import fin_defs
@ -197,8 +203,10 @@ def sample_from_range(rng: random.Random, rang: tuple[Any, Any]) -> Any:
return rang[0] + (rang[1] - rang[0]) * multiplier
def run_auto_sell(config: AutoSellConfig) -> 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
@ -210,39 +218,65 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults:
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 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(QUANTIZE_DECIMALS)
logger.info('Selling %s %s', amount_to_sell, config.input_asset.raw_short_name())
order_details = config.seller.place_market_order(
fin_defs.AssetAmount(config.input_asset, amount_to_sell),
config.output_asset,
try:
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(),
)
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(
QUANTIZE_DECIMALS,
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)
@ -253,6 +287,68 @@ 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],
)
minisleep = 0.1
logger.info('')
logger.info('Welcome to crypto seller!')
config.sleep(3)
logger.info('')
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(3)
logger.info('')
config.sleep(minisleep)
logger.info(
'- Current balance: %s %s',
current_balance,
config.input_asset.raw_short_name(),
)
config.sleep(minisleep)
logger.info('- Average sleep: %s seconds', average_sleep)
config.sleep(minisleep)
logger.info(
'- Average amount: %s %s',
average_amount,
config.input_asset.raw_short_name(),
)
config.sleep(minisleep)
logger.info('- Expected counts: %s', expected_num_sell_offs)
config.sleep(minisleep)
logger.info('- Expected time: %s', expected_duration)
config.sleep(minisleep)
logger.info('- Fastest time: %s', fastest_duration)
config.sleep(minisleep)
logger.info('- Slowest time: %s', slowest_duration)
config.sleep(minisleep)
logger.info('')
config.sleep(minisleep)
logger.info('Do you still want to proceed?')
config.sleep(minisleep)
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))

View File

@ -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
@ -12,6 +13,7 @@ import fin_depo
from . import (
AutoSellConfig,
log_estimates,
log_results,
order_csv,
run_auto_sell,
@ -20,6 +22,12 @@ from . import logger as module_logger
logger = logging.getLogger(__name__)
################################################################################
# Constants #
PATH_FILE_LOG = 'log.txt'
PATH_FILE_TRADES = 'trades.csv'
################################################################################
# Application Setup #
@ -50,7 +58,14 @@ def setup_logging(path_log_file: Path):
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()
@ -88,9 +103,32 @@ def load_config(config_path: Path, path_trades_file: 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('--output', type=Path, dest='output_folder', required=True)
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(
'--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',
dest='wait_before_first',
help='Skip the first sell-off round, and wait for the next.',
)
return parser.parse_args()
@ -110,6 +148,20 @@ def main():
auto_sell_config = load_config(args.config_file, path_trades_file)
results = run_auto_sell(auto_sell_config)
# Display estimates
try:
log_estimates(auto_sell_config)
except KeyboardInterrupt:
sys.exit(1)
# Run auto sell
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')
log_results(results)

3
pytest.ini Normal file
View File

@ -0,0 +1,3 @@
[pytest]
log_cli = True
log_level=INFO

1
requirements_test.txt Normal file
View File

@ -0,0 +1 @@
pytest

View File

@ -61,11 +61,13 @@ 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,
)
crypto_seller.log_estimates(config)
results = crypto_seller.run_auto_sell(config)
# Check results
@ -76,4 +78,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 + 17.2