288 lines
8.3 KiB
Python
288 lines
8.3 KiB
Python
import argparse
|
|
import logging
|
|
import shutil
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
|
|
from pricehist import __version__, outputs, sources
|
|
from pricehist.fetch import fetch
|
|
from pricehist.format import Format
|
|
from pricehist.series import Series
|
|
|
|
|
|
def cli(args=None, output_file=sys.stdout):
|
|
start_time = datetime.now()
|
|
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
|
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
|
|
if args.debug:
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
elif args.verbose:
|
|
logging.getLogger().setLevel(logging.INFO)
|
|
|
|
logging.debug(f"Started pricehist run at {start_time}.")
|
|
|
|
try:
|
|
if args.version:
|
|
print(f"pricehist v{__version__}", file=output_file)
|
|
elif args.command == "sources":
|
|
result = sources.formatted()
|
|
print(result, file=output_file)
|
|
elif args.command == "source" and args.symbols:
|
|
result = sources.by_id[args.source].format_symbols()
|
|
print(result, file=output_file)
|
|
elif args.command == "source":
|
|
total_width = shutil.get_terminal_size().columns
|
|
result = sources.by_id[args.source].format_info(total_width)
|
|
print(result, file=output_file)
|
|
elif args.command == "fetch":
|
|
source = sources.by_id[args.source]
|
|
output = outputs.by_type[args.output]
|
|
series = Series(
|
|
base=args.pair.split("/")[0],
|
|
quote=args.pair.split("/")[1],
|
|
type=args.type or (source.types() + ["unknown"])[0],
|
|
start=args.start or source.start(),
|
|
end=args.end,
|
|
)
|
|
fmt = Format.generate(args)
|
|
result = fetch(series, source, output, args.invert, args.quantize, fmt)
|
|
print(result, end="", file=output_file)
|
|
else:
|
|
parser.print_help(file=sys.stderr)
|
|
except BrokenPipeError:
|
|
logging.debug("The output pipe was closed early.")
|
|
|
|
logging.debug(f"Finished pricehist run at {datetime.now()}.")
|
|
|
|
|
|
def build_parser():
|
|
def valid_date(s):
|
|
if s == "today":
|
|
return today()
|
|
try:
|
|
datetime.strptime(s, "%Y-%m-%d")
|
|
return s
|
|
except ValueError:
|
|
msg = "Not a valid date: '{0}'.".format(s)
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
def previous_valid_date(s):
|
|
return str(
|
|
datetime.strptime(valid_date(s), "%Y-%m-%d").date() - timedelta(days=1)
|
|
)
|
|
|
|
def following_valid_date(s):
|
|
return str(
|
|
datetime.strptime(valid_date(s), "%Y-%m-%d").date() + timedelta(days=1)
|
|
)
|
|
|
|
def today():
|
|
return str(datetime.now().date())
|
|
|
|
def formatter(prog):
|
|
return argparse.HelpFormatter(prog, max_help_position=50)
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog="pricehist",
|
|
description="Fetch historical price data",
|
|
formatter_class=formatter,
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--version",
|
|
action="store_true",
|
|
help="show version information",
|
|
)
|
|
|
|
logging_group = parser.add_mutually_exclusive_group(required=False)
|
|
logging_group.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="show INFO messages",
|
|
)
|
|
logging_group.add_argument(
|
|
"--debug",
|
|
action="store_true",
|
|
help="show INFO and DEBUG messages",
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(title="commands", dest="command")
|
|
|
|
subparsers.add_parser(
|
|
"sources",
|
|
help="list sources",
|
|
formatter_class=formatter,
|
|
)
|
|
|
|
source_parser = subparsers.add_parser(
|
|
"source",
|
|
help="show source details",
|
|
usage="pricehist source SOURCE [-h] [-s]",
|
|
formatter_class=formatter,
|
|
)
|
|
source_parser.add_argument(
|
|
"source",
|
|
metavar="SOURCE",
|
|
type=str,
|
|
choices=sources.by_id.keys(),
|
|
help="the source identifier",
|
|
)
|
|
source_parser.add_argument(
|
|
"-s",
|
|
"--symbols",
|
|
action="store_true",
|
|
help="list available symbols",
|
|
)
|
|
|
|
fetch_parser = subparsers.add_parser(
|
|
"fetch",
|
|
help="fetch prices",
|
|
usage=(
|
|
# Set usage manually to have positional arguments before options
|
|
# and show allowed values where appropriate
|
|
"pricehist fetch SOURCE PAIR [-h] "
|
|
"[-t TYPE] [-s DATE | -sx DATE] [-e DATE | -ex DATE] "
|
|
f"[-o {'|'.join(outputs.by_type.keys())}] "
|
|
"[--invert] [--quantize INT] "
|
|
"[--fmt-base SYM] [--fmt-quote SYM] [--fmt-time TIME] "
|
|
"[--fmt-decimal CHAR] [--fmt-thousands CHAR] "
|
|
"[--fmt-symbol rightspace|right|leftspace|left] [--fmt-datesep CHAR]"
|
|
),
|
|
formatter_class=formatter,
|
|
)
|
|
fetch_parser.add_argument(
|
|
"source",
|
|
metavar="SOURCE",
|
|
type=str,
|
|
choices=sources.by_id.keys(),
|
|
help="the source identifier",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"pair",
|
|
metavar="PAIR",
|
|
type=str,
|
|
help="symbols in the form BASE/QUOTE, e.g. BTC/USD",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"-t",
|
|
"--type",
|
|
dest="type",
|
|
metavar="TYPE",
|
|
type=str,
|
|
help="price type, e.g. close",
|
|
)
|
|
fetch_start_group = fetch_parser.add_mutually_exclusive_group(required=False)
|
|
fetch_start_group.add_argument(
|
|
"-s",
|
|
"--start",
|
|
dest="start",
|
|
metavar="DATE",
|
|
type=valid_date,
|
|
help="start date, inclusive (default: source start)",
|
|
)
|
|
fetch_start_group.add_argument(
|
|
"-sx",
|
|
"--startx",
|
|
dest="start",
|
|
metavar="DATE",
|
|
type=following_valid_date,
|
|
help="start date, exclusive",
|
|
)
|
|
|
|
fetch_end_group = fetch_parser.add_mutually_exclusive_group(required=False)
|
|
fetch_end_group.add_argument(
|
|
"-e",
|
|
"--end",
|
|
dest="end",
|
|
metavar="DATE",
|
|
type=valid_date,
|
|
default=today(),
|
|
help="end date, inclusive (default: today)",
|
|
)
|
|
fetch_end_group.add_argument(
|
|
"-ex",
|
|
"--endx",
|
|
dest="end",
|
|
metavar="DATE",
|
|
type=previous_valid_date,
|
|
help="end date, exclusive",
|
|
)
|
|
|
|
fetch_parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
dest="output",
|
|
metavar="FMT",
|
|
type=str,
|
|
choices=outputs.by_type.keys(),
|
|
default=outputs.default,
|
|
help=f"output format (default: {outputs.default})",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--invert",
|
|
action="store_true",
|
|
help="invert the price, swapping base and quote",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--quantize",
|
|
dest="quantize",
|
|
metavar="INT",
|
|
type=int,
|
|
help="round to the given number of decimal places",
|
|
)
|
|
default_fmt = Format()
|
|
fetch_parser.add_argument(
|
|
"--fmt-base",
|
|
dest="formatbase",
|
|
metavar="SYM",
|
|
type=str,
|
|
help="rename the base symbol in output",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--fmt-quote",
|
|
dest="formatquote",
|
|
metavar="SYM",
|
|
type=str,
|
|
help="rename the quote symbol in output",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--fmt-time",
|
|
dest="formattime",
|
|
metavar="TIME",
|
|
type=str,
|
|
help=f"set a particular time of day in output (default: {default_fmt.time})",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--fmt-decimal",
|
|
dest="formatdecimal",
|
|
metavar="CHAR",
|
|
type=str,
|
|
help=f"decimal point in output (default: '{default_fmt.decimal}')",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--fmt-thousands",
|
|
dest="formatthousands",
|
|
metavar="CHAR",
|
|
type=str,
|
|
help=f"thousands separator in output (default: '{default_fmt.thousands}')",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--fmt-symbol",
|
|
dest="formatsymbol",
|
|
metavar="LOCATION",
|
|
type=str,
|
|
choices=["rightspace", "right", "leftspace", "left"],
|
|
help=f"commodity symbol placement in output (default: {default_fmt.symbol})",
|
|
)
|
|
fetch_parser.add_argument(
|
|
"--fmt-datesep",
|
|
dest="formatdatesep",
|
|
metavar="CHAR",
|
|
type=str,
|
|
help=f"date separator in output (default: '{default_fmt.datesep}')",
|
|
)
|
|
|
|
return parser
|