Compare commits
No commits in common. "684e08de0432b9bb2876b8f8ceb1fec1b47ddcad" and "0f4397d9e541b7ee5721a67fddbebac71b19c819" have entirely different histories.
684e08de04
...
0f4397d9e5
|
@ -95,20 +95,14 @@ most mature on the danish market, and does support KuCoin.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [X] Allow multiple secrets configs
|
- [ ] Present an overview of what the script will be doing
|
||||||
- [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?
|
* Sell what for what? How much, how often?
|
||||||
* Give an estimate of how long it will take.
|
* Give an estimate of how long it will take.
|
||||||
* Wait 20 seconds before starting, to allow the user to review.
|
* Wait 20 seconds before starting, to allow the user to review.
|
||||||
- [X] Document command-line arguments
|
- [ ] 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] Document configuration
|
- [X] Document configuration
|
||||||
- [X] Document code auditing
|
- [X] Document code auditing
|
||||||
- [X] Parse configuration from json.
|
- [X] Parse configuration from json.
|
||||||
|
@ -125,7 +119,7 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from decimal import ROUND_DOWN, Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import fin_defs
|
import fin_defs
|
||||||
|
@ -203,10 +197,8 @@ 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(
|
|
||||||
config: AutoSellConfig,
|
def run_auto_sell(config: AutoSellConfig) -> AutoSellRunResults:
|
||||||
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
|
||||||
|
@ -218,33 +210,18 @@ def run_auto_sell(
|
||||||
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)
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
while True:
|
||||||
# Check that account has tokens.
|
# Check that account has tokens.
|
||||||
input_amount_available = config.seller.get_depo().get_amount_of_asset(
|
input_amount_available = config.seller.get_depo().get_amount_of_asset(
|
||||||
config.input_asset,
|
config.input_asset,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info('Currently own %s %s', input_amount_available, config.input_asset.raw_short_name())
|
||||||
'Currently own %s %s',
|
|
||||||
input_amount_available,
|
|
||||||
config.input_asset.raw_short_name(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if initial_rounds_to_skip > 0:
|
if input_amount_available > 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 = sample_from_range(rng, config.input_amount_range)
|
||||||
amount_to_sell = min(input_amount_available, amount_to_sell)
|
amount_to_sell = min(input_amount_available, amount_to_sell)
|
||||||
amount_to_sell = amount_to_sell.quantize(
|
amount_to_sell = amount_to_sell.quantize(QUANTIZE_DECIMALS)
|
||||||
QUANTIZE_DECIMALS,
|
logger.info('Selling %s %s', amount_to_sell, config.input_asset.raw_short_name())
|
||||||
rounding=ROUND_DOWN,
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
'Selling %s %s',
|
|
||||||
amount_to_sell,
|
|
||||||
config.input_asset.raw_short_name(),
|
|
||||||
)
|
|
||||||
|
|
||||||
order_details = config.seller.place_market_order(
|
order_details = config.seller.place_market_order(
|
||||||
fin_defs.AssetAmount(config.input_asset, amount_to_sell),
|
fin_defs.AssetAmount(config.input_asset, amount_to_sell),
|
||||||
|
@ -266,16 +243,6 @@ def run_auto_sell(
|
||||||
total_sleep_duration += time_to_sleep
|
total_sleep_duration += time_to_sleep
|
||||||
|
|
||||||
del input_amount_available
|
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)
|
||||||
|
|
||||||
|
@ -286,68 +253,6 @@ def run_auto_sell(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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))
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -13,7 +12,6 @@ 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,
|
||||||
|
@ -22,12 +20,6 @@ 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 #
|
||||||
|
|
||||||
|
@ -58,14 +50,7 @@ def setup_logging(path_log_file: Path):
|
||||||
|
|
||||||
|
|
||||||
CLI_DESCRIPTION = """
|
CLI_DESCRIPTION = """
|
||||||
Script to automatically and with little effort sell financial assets from
|
Sells financial assets from an online account.
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,32 +88,9 @@ def load_config(config_path: Path, path_trades_file: Path) -> AutoSellConfig:
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser('crypto_seller', description=CLI_DESCRIPTION)
|
||||||
prog='crypto_seller',
|
parser.add_argument('--config', type=Path, dest='config_file', required=True)
|
||||||
description=CLI_DESCRIPTION,
|
parser.add_argument('--output', type=Path, dest='output_folder', required=True)
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,20 +110,6 @@ 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)
|
||||||
|
|
||||||
|
|
|
@ -26,24 +26,18 @@ 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
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[pytest]
|
|
||||||
log_cli = True
|
|
||||||
log_level=INFO
|
|
|
@ -1 +0,0 @@
|
||||||
pytest
|
|
|
@ -61,13 +61,11 @@ 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('test/output/test-trades.csv'),
|
Path('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
|
||||||
|
@ -78,4 +76,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 + 17.2
|
assert sleep_mock.time_slept == 1000
|
||||||
|
|
Loading…
Reference in New Issue
Block a user