1
0

Compare commits

...

11 Commits

Author SHA1 Message Date
684e08de04
Fixed logging
Some checks failed
Python Ruff Code Quality / ruff (push) Failing after 21s
Run Python tests (through Pytest) / Test (push) Failing after 29s
Verify Python project can be installed, loaded and have version checked / Test (push) Failing after 27s
2024-12-02 18:18:20 +01:00
b55a7ad3e0
Found some changes that were stuck between the commits. 2024-12-02 18:08:27 +01:00
92c7f3e541
Output and secrets are now configurable
All checks were successful
Test Python / Test (push) Successful in 27s
2024-09-04 20:04:37 +02:00
48a821a8d4
Improved error recovery 2024-09-04 19:48:29 +02:00
9709c9b68a
Ruff
All checks were successful
Test Python / Test (push) Successful in 28s
2024-09-04 17:55:02 +02:00
af07c75820
Improved CLI 2024-09-04 17:53:06 +02:00
c8ce383751
Added --skip-first command line argument
All checks were successful
Test Python / Test (push) Successful in 27s
2024-09-04 17:36:33 +02:00
1d7327fe6a Estimates will now be logged during startup
All checks were successful
Test Python / Test (push) Successful in 28s
2024-09-04 15:36:00 +02:00
97369e0b74
Quantize correctly
All checks were successful
Test Python / Test (push) Successful in 26s
2024-09-02 20:26:17 +02:00
f82276eac7
Rounding
Some checks failed
Test Python / Test (push) Failing after 26s
2024-09-02 20:09:09 +02:00
617514c82e 🤖 Bumped version to 0.1.10
All checks were successful
Package Python / Package (push) Successful in 24s
Test Python / Test (push) Successful in 26s
This commit was automatically generated by a script: https://gitfub.space/Jmaa/python-omni
2024-09-01 19:00:07 +02:00
6 changed files with 212 additions and 53 deletions

View File

@ -95,14 +95,20 @@ most mature on the danish market, and does support KuCoin.
## TODO ## TODO
- [ ] Present an overview of what the script will be doing - [X] Allow multiple secrets configs
* Sell what for what? How much, how often? - [X] Allow multiple output directories
* Give an estimate of how long it will take. - [X] Fix `TimeoutError` issue occuring from slow Kucoin endpoint. Might
* Wait 20 seconds before starting, to allow the user to review. require implementing own kucoin backend in `fin_depo`.
- [ ] Ensure that a failure during selling results in a safe winding down of the system. - [X] Ensure that a failure during selling results in a safe winding down of the system.
* Catch runtime errors when selling * Catch runtime errors when selling
* Show errors to log. * Show status
* Stop loop and exit with results, and error indicator. * 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 configuration
- [X] Document code auditing - [X] Document code auditing
- [X] Parse configuration from json. - [X] Parse configuration from json.
@ -119,7 +125,7 @@ import datetime
import logging import logging
import random import random
from collections.abc import Callable from collections.abc import Callable
from decimal import Decimal from decimal import ROUND_DOWN, Decimal
from typing import Any from typing import Any
import fin_defs 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 return rang[0] + (rang[1] - rang[0]) * multiplier
def run_auto_sell(
def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults: config: AutoSellConfig,
initial_rounds_to_skip: int = 0,
) -> AutoSellRunResults:
"""Executes the sell-off. """Executes the sell-off.
Sell-offs are performed in rounds of sizes and with intervals randomly Sell-offs are performed in rounds of sizes and with intervals randomly
@ -210,39 +218,64 @@ def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults:
all_executed_orders: list[fin_depo.data.TradeOrderDetails] = [] all_executed_orders: list[fin_depo.data.TradeOrderDetails] = []
total_sleep_duration = datetime.timedelta(seconds=0) total_sleep_duration = datetime.timedelta(seconds=0)
while True: try:
# Check that account has tokens. while True:
input_amount_available = config.seller.get_depo().get_amount_of_asset( # Check that account has tokens.
config.input_asset, 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(
if input_amount_available > 0: 'Currently own %s %s',
amount_to_sell = sample_from_range(rng, config.input_amount_range) input_amount_available,
amount_to_sell = min(input_amount_available, amount_to_sell) config.input_asset.raw_short_name(),
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,
) )
config.log_order_to_csv(order_details) if initial_rounds_to_skip > 0:
all_executed_orders.append(order_details) 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(
'Selling %s %s',
amount_to_sell,
config.input_asset.raw_short_name(),
)
del amount_to_sell order_details = config.seller.place_market_order(
elif config.exit_when_empty: fin_defs.AssetAmount(config.input_asset, amount_to_sell),
break config.output_asset,
)
# Time out config.log_order_to_csv(order_details)
time_to_sleep = sample_from_range(rng, config.interval_range) all_executed_orders.append(order_details)
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 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) time_end = datetime.datetime.now(tz=datetime.UTC)
@ -253,6 +286,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): def log_results(results: AutoSellRunResults):
logger.info('Stats:') logger.info('Stats:')
logger.info('- Num orders: %s', len(results.order_details)) logger.info('- Num orders: %s', len(results.order_details))

View File

@ -3,6 +3,7 @@ import datetime
import json import json
import logging import logging
import logging.handlers import logging.handlers
import sys
import time import time
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
@ -12,6 +13,7 @@ import fin_depo
from . import ( from . import (
AutoSellConfig, AutoSellConfig,
log_estimates,
log_results, log_results,
order_csv, order_csv,
run_auto_sell, run_auto_sell,
@ -20,6 +22,12 @@ from . import logger as module_logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
################################################################################
# Constants #
PATH_FILE_LOG = 'log.txt'
PATH_FILE_TRADES = 'trades.csv'
################################################################################ ################################################################################
# Application Setup # # Application Setup #
@ -50,7 +58,14 @@ def setup_logging(path_log_file: Path):
CLI_DESCRIPTION = """ 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() """.strip()
@ -88,9 +103,32 @@ def load_config(config_path: Path, path_trades_file: Path) -> AutoSellConfig:
def parse_args(): def parse_args():
parser = argparse.ArgumentParser('crypto_seller', description=CLI_DESCRIPTION) parser = argparse.ArgumentParser(
parser.add_argument('--config', type=Path, dest='config_file', required=True) prog='crypto_seller',
parser.add_argument('--output', type=Path, dest='output_folder', required=True) 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() return parser.parse_args()
@ -110,6 +148,20 @@ def main():
auto_sell_config = load_config(args.config_file, path_trades_file) auto_sell_config = load_config(args.config_file, path_trades_file)
results = run_auto_sell(auto_sell_config) 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') logging.info('Sell-offs complete')
log_results(results) log_results(results)

View File

@ -26,18 +26,24 @@ class CsvFileLogger:
'raw_order_details', 'raw_order_details',
] ]
# Select data to write
d = {
'executed_time': trade_order.executed_time,
'input_asset': trade_order.input.asset,
'input_amount': trade_order.input.amount,
'output_asset': trade_order.output.asset,
'output_amount': trade_order.output.amount,
'fee_asset': trade_order.fee.asset,
'fee_amount': trade_order.fee.amount,
'order_id': trade_order.order_id,
'raw_order_details': trade_order.raw_order_details,
}
# Ensure that directory exists # Ensure that directory exists
self.path.parent.mkdir(parents=True, exist_ok=True) self.path.parent.mkdir(parents=True, exist_ok=True)
# Write row in CSV file # Write row in CSV file
with open(self.path, 'a') as f: with open(self.path, 'a') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames) writer = csv.DictWriter(f, fieldnames=fieldnames)
d = trade_order.__dict__
d['input_asset'] = d['input'].asset
d['input_amount'] = d['input'].amount
d['output_asset'] = d['output'].asset
d['output_amount'] = d['output'].amount
d['fee_asset'] = d['fee'].asset
d['fee_amount'] = d['fee'].amount
del d['fee'], d['input'], d['output']
writer.writerow(d) writer.writerow(d)
del d

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, exit_when_empty=True,
seller=seller_mock, seller=seller_mock,
log_order_to_csv=crypto_seller.order_csv.CsvFileLogger( log_order_to_csv=crypto_seller.order_csv.CsvFileLogger(
Path('output/test-trades.csv'), Path('test/output/test-trades.csv'),
), ),
sleep=sleep_mock, sleep=sleep_mock,
) )
crypto_seller.log_estimates(config)
results = crypto_seller.run_auto_sell(config) results = crypto_seller.run_auto_sell(config)
# Check results # Check results
@ -76,4 +78,4 @@ def test_auto_run():
# Check mocks agree # Check mocks agree
assert seller_mock.amount_left == 0 assert seller_mock.amount_left == 0
assert sleep_mock.time_slept == 1000 assert sleep_mock.time_slept == 1000 + 17.2