After running Black.
This commit is contained in:
parent
4f9353013e
commit
d2416ebb0c
13 changed files with 203 additions and 121 deletions
|
@ -1 +1 @@
|
||||||
__version__ = '0.1.0'
|
__version__ = "0.1.0"
|
||||||
|
|
|
@ -5,33 +5,37 @@ from datetime import datetime, timedelta
|
||||||
from pricehist import sources
|
from pricehist import sources
|
||||||
from pricehist import outputs
|
from pricehist import outputs
|
||||||
|
|
||||||
|
|
||||||
def cli(args=None):
|
def cli(args=None):
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if (args.command == 'sources'):
|
if args.command == "sources":
|
||||||
cmd_sources(args)
|
cmd_sources(args)
|
||||||
elif (args.command == 'source'):
|
elif args.command == "source":
|
||||||
cmd_source(args)
|
cmd_source(args)
|
||||||
elif (args.command == 'fetch'):
|
elif args.command == "fetch":
|
||||||
cmd_fetch(args)
|
cmd_fetch(args)
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
def cmd_sources(args):
|
def cmd_sources(args):
|
||||||
width = max([len(identifier) for identifier in sources.by_id.keys()])
|
width = max([len(identifier) for identifier in sources.by_id.keys()])
|
||||||
for identifier, source in sources.by_id.items():
|
for identifier, source in sources.by_id.items():
|
||||||
print(f'{identifier.ljust(width)} {source.name()}')
|
print(f"{identifier.ljust(width)} {source.name()}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_source(args):
|
def cmd_source(args):
|
||||||
source = sources.by_id[args.identifier]
|
source = sources.by_id[args.identifier]
|
||||||
print(f'ID : {source.id()}')
|
print(f"ID : {source.id()}")
|
||||||
print(f'Name : {source.name()}')
|
print(f"Name : {source.name()}")
|
||||||
print(f'Description : {source.description()}')
|
print(f"Description : {source.description()}")
|
||||||
print(f'URL : {source.source_url()}')
|
print(f"URL : {source.source_url()}")
|
||||||
print(f'Bases : {", ".join(source.bases())}')
|
print(f'Bases : {", ".join(source.bases())}')
|
||||||
print(f'Quotes : {", ".join(source.quotes())}')
|
print(f'Quotes : {", ".join(source.quotes())}')
|
||||||
|
|
||||||
|
|
||||||
def cmd_fetch(args):
|
def cmd_fetch(args):
|
||||||
source = sources.by_id[args.source]()
|
source = sources.by_id[args.source]()
|
||||||
start = args.start or args.after
|
start = args.start or args.after
|
||||||
|
@ -39,9 +43,10 @@ def cmd_fetch(args):
|
||||||
prices = source.fetch(args.pair, args.start, args.end)
|
prices = source.fetch(args.pair, args.start, args.end)
|
||||||
print(output.format(prices))
|
print(output.format(prices))
|
||||||
|
|
||||||
|
|
||||||
def build_parser():
|
def build_parser():
|
||||||
def valid_date(s):
|
def valid_date(s):
|
||||||
if s == 'today':
|
if s == "today":
|
||||||
return today()
|
return today()
|
||||||
try:
|
try:
|
||||||
datetime.strptime(s, "%Y-%m-%d")
|
datetime.strptime(s, "%Y-%m-%d")
|
||||||
|
@ -51,40 +56,83 @@ def build_parser():
|
||||||
raise argparse.ArgumentTypeError(msg)
|
raise argparse.ArgumentTypeError(msg)
|
||||||
|
|
||||||
def following_valid_date(s):
|
def following_valid_date(s):
|
||||||
return str(datetime.strptime(valid_date(s), "%Y-%m-%d").date() + timedelta(days=1))
|
return str(
|
||||||
|
datetime.strptime(valid_date(s), "%Y-%m-%d").date() + timedelta(days=1)
|
||||||
|
)
|
||||||
|
|
||||||
def today():
|
def today():
|
||||||
return str(datetime.now().date())
|
return str(datetime.now().date())
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Fetch historical price data')
|
parser = argparse.ArgumentParser(description="Fetch historical price data")
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(title='commands', dest='command')
|
subparsers = parser.add_subparsers(title="commands", dest="command")
|
||||||
|
|
||||||
sources_parser = subparsers.add_parser('sources', help='list sources')
|
sources_parser = subparsers.add_parser("sources", help="list sources")
|
||||||
|
|
||||||
source_parser = subparsers.add_parser('source', help='show source details')
|
source_parser = subparsers.add_parser("source", help="show source details")
|
||||||
source_parser.add_argument('identifier', metavar='ID', type=str,
|
source_parser.add_argument(
|
||||||
|
"identifier",
|
||||||
|
metavar="ID",
|
||||||
|
type=str,
|
||||||
choices=sources.by_id.keys(),
|
choices=sources.by_id.keys(),
|
||||||
help='the source identifier')
|
help="the source identifier",
|
||||||
|
)
|
||||||
|
|
||||||
fetch_parser = subparsers.add_parser('fetch', help='fetch prices',
|
fetch_parser = subparsers.add_parser(
|
||||||
usage='pricehist fetch ID [-h] -p PAIR (-s DATE | -sx DATE) [-e DATE] [-o FMT]')
|
"fetch",
|
||||||
fetch_parser.add_argument('source', metavar='ID', type=str,
|
help="fetch prices",
|
||||||
|
usage="pricehist fetch ID [-h] -p PAIR (-s DATE | -sx DATE) [-e DATE] [-o FMT]",
|
||||||
|
)
|
||||||
|
fetch_parser.add_argument(
|
||||||
|
"source",
|
||||||
|
metavar="ID",
|
||||||
|
type=str,
|
||||||
choices=sources.by_id.keys(),
|
choices=sources.by_id.keys(),
|
||||||
help='the source identifier')
|
help="the source identifier",
|
||||||
fetch_parser.add_argument('-p', '--pair', dest='pair', type=str, required=True,
|
)
|
||||||
help='pair, usually BASE/QUOTE, e.g. BTC/USD')
|
fetch_parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--pair",
|
||||||
|
dest="pair",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="pair, usually BASE/QUOTE, e.g. BTC/USD",
|
||||||
|
)
|
||||||
fetch_start_group = fetch_parser.add_mutually_exclusive_group(required=True)
|
fetch_start_group = fetch_parser.add_mutually_exclusive_group(required=True)
|
||||||
fetch_start_group.add_argument('-s', '--start', dest='start', metavar='DATE', type=valid_date,
|
fetch_start_group.add_argument(
|
||||||
help='start date, inclusive')
|
"-s",
|
||||||
fetch_start_group.add_argument('-sx', '--startx', dest='start', metavar='DATE', type=following_valid_date,
|
"--start",
|
||||||
help='start date, exclusive')
|
dest="start",
|
||||||
fetch_parser.add_argument('-e', '--end', dest='end', metavar='DATE', type=valid_date,
|
metavar="DATE",
|
||||||
|
type=valid_date,
|
||||||
|
help="start date, inclusive",
|
||||||
|
)
|
||||||
|
fetch_start_group.add_argument(
|
||||||
|
"-sx",
|
||||||
|
"--startx",
|
||||||
|
dest="start",
|
||||||
|
metavar="DATE",
|
||||||
|
type=following_valid_date,
|
||||||
|
help="start date, exclusive",
|
||||||
|
)
|
||||||
|
fetch_parser.add_argument(
|
||||||
|
"-e",
|
||||||
|
"--end",
|
||||||
|
dest="end",
|
||||||
|
metavar="DATE",
|
||||||
|
type=valid_date,
|
||||||
default=today(),
|
default=today(),
|
||||||
help='end date, inclusive (default: today)')
|
help="end date, inclusive (default: today)",
|
||||||
fetch_parser.add_argument('-o', '--output', dest='output', metavar='FMT', type=str,
|
)
|
||||||
|
fetch_parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
dest="output",
|
||||||
|
metavar="FMT",
|
||||||
|
type=str,
|
||||||
choices=outputs.by_type.keys(),
|
choices=outputs.by_type.keys(),
|
||||||
default=outputs.default,
|
default=outputs.default,
|
||||||
help=f'output format (default: {outputs.default})')
|
help=f"output format (default: {outputs.default})",
|
||||||
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
|
@ -3,11 +3,11 @@ from .csv import CSV
|
||||||
from .gnucashsql import GnuCashSQL
|
from .gnucashsql import GnuCashSQL
|
||||||
from .ledger import Ledger
|
from .ledger import Ledger
|
||||||
|
|
||||||
default = 'ledger'
|
default = "ledger"
|
||||||
|
|
||||||
by_type = {
|
by_type = {
|
||||||
'beancount': Beancount,
|
"beancount": Beancount,
|
||||||
'csv': CSV,
|
"csv": CSV,
|
||||||
'gnucash-sql': GnuCashSQL,
|
"gnucash-sql": GnuCashSQL,
|
||||||
'ledger': Ledger
|
"ledger": Ledger,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
class Beancount():
|
class Beancount:
|
||||||
|
|
||||||
def format(self, prices):
|
def format(self, prices):
|
||||||
lines = []
|
lines = []
|
||||||
for price in prices:
|
for price in prices:
|
||||||
date = str(price.date).translate(str.maketrans('-','/'))
|
date = str(price.date).translate(str.maketrans("-", "/"))
|
||||||
lines.append(f"{price.date} price {price.base} {price.amount} {price.quote}")
|
lines.append(
|
||||||
|
f"{price.date} price {price.base} {price.amount} {price.quote}"
|
||||||
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# https://beancount.github.io/docs/fetching_prices_in_beancount.html
|
# https://beancount.github.io/docs/fetching_prices_in_beancount.html
|
||||||
# https://beancount.github.io/docs/beancount_language_syntax.html#commodities-currencies
|
# https://beancount.github.io/docs/beancount_language_syntax.html#commodities-currencies
|
||||||
# https://beancount.github.io/docs/beancount_language_syntax.html#comments
|
# https://beancount.github.io/docs/beancount_language_syntax.html#comments
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
class CSV():
|
class CSV:
|
||||||
|
|
||||||
def format(self, prices):
|
def format(self, prices):
|
||||||
lines = ["date,base,quote,amount"]
|
lines = ["date,base,quote,amount"]
|
||||||
for price in prices:
|
for price in prices:
|
||||||
date = str(price.date).translate(str.maketrans('-','/'))
|
date = str(price.date).translate(str.maketrans("-", "/"))
|
||||||
line = ','.join([price.date, price.base, price.quote, str(price.amount)])
|
line = ",".join([price.date, price.base, price.quote, str(price.amount)])
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
|
@ -3,25 +3,29 @@ import hashlib
|
||||||
|
|
||||||
from pricehist import __version__
|
from pricehist import __version__
|
||||||
|
|
||||||
class GnuCashSQL():
|
|
||||||
|
|
||||||
|
class GnuCashSQL:
|
||||||
def format(self, prices):
|
def format(self, prices):
|
||||||
source = 'pricehist'
|
source = "pricehist"
|
||||||
typ = 'unknown'
|
typ = "unknown"
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
for price in prices:
|
for price in prices:
|
||||||
date = f'{price.date} 00:00:00'
|
date = f"{price.date} 00:00:00"
|
||||||
m = hashlib.sha256()
|
m = hashlib.sha256()
|
||||||
m.update("".join([date, price.base, price.quote, source, typ, str(price.amount)]).encode('utf-8'))
|
m.update(
|
||||||
|
"".join(
|
||||||
|
[date, price.base, price.quote, source, typ, str(price.amount)]
|
||||||
|
).encode("utf-8")
|
||||||
|
)
|
||||||
guid = m.hexdigest()[0:32]
|
guid = m.hexdigest()[0:32]
|
||||||
value_num = str(price.amount).replace('.', '')
|
value_num = str(price.amount).replace(".", "")
|
||||||
value_denom = 10 ** len(f'{price.amount}.'.split('.')[1])
|
value_denom = 10 ** len(f"{price.amount}.".split(".")[1])
|
||||||
v = f"('{guid}', '{date}', '{price.base}', '{price.quote}', '{source}', '{typ}', {value_num}, {value_denom})"
|
v = f"('{guid}', '{date}', '{price.base}', '{price.quote}', '{source}', '{typ}', {value_num}, {value_denom})"
|
||||||
values.append(v)
|
values.append(v)
|
||||||
|
|
||||||
comma_newline = ",\n"
|
comma_newline = ",\n"
|
||||||
sql = f'''\
|
sql = f"""\
|
||||||
-- Created by pricehist v{__version__} at {datetime.utcnow().isoformat()}Z
|
-- Created by pricehist v{__version__} at {datetime.utcnow().isoformat()}Z
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
@ -66,6 +70,6 @@ SELECT * FROM summary;
|
||||||
SELECT 'final' AS status, p.* FROM prices p WHERE p.guid IN (SELECT guid FROM new_prices) ORDER BY p.date;
|
SELECT 'final' AS status, p.* FROM prices p WHERE p.guid IN (SELECT guid FROM new_prices) ORDER BY p.date;
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
'''
|
"""
|
||||||
|
|
||||||
return sql
|
return sql
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
class Ledger():
|
class Ledger:
|
||||||
|
|
||||||
def format(self, prices):
|
def format(self, prices):
|
||||||
lines = []
|
lines = []
|
||||||
for price in prices:
|
for price in prices:
|
||||||
date = str(price.date).translate(str.maketrans('-','/'))
|
date = str(price.date).translate(str.maketrans("-", "/"))
|
||||||
lines.append(f"P {date} 00:00:00 {price.base} {price.amount} {price.quote}")
|
lines.append(f"P {date} 00:00:00 {price.base} {price.amount} {price.quote}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
# TODO support additional details of the format:
|
# TODO support additional details of the format:
|
||||||
# https://www.ledger-cli.org/3.0/doc/ledger3.html#Commodities-and-Currencies
|
# https://www.ledger-cli.org/3.0/doc/ledger3.html#Commodities-and-Currencies
|
||||||
# https://www.ledger-cli.org/3.0/doc/ledger3.html#Commoditized-Amounts
|
# https://www.ledger-cli.org/3.0/doc/ledger3.html#Commoditized-Amounts
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
Price = namedtuple('Price', ['base', 'quote', 'date', 'amount'])
|
Price = namedtuple("Price", ["base", "quote", "date", "amount"])
|
||||||
|
|
|
@ -2,8 +2,4 @@ from .coindesk import CoinDesk
|
||||||
from .coinmarketcap import CoinMarketCap
|
from .coinmarketcap import CoinMarketCap
|
||||||
from .ecb import ECB
|
from .ecb import ECB
|
||||||
|
|
||||||
by_id = {
|
by_id = {CoinDesk.id(): CoinDesk, CoinMarketCap.id(): CoinMarketCap, ECB.id(): ECB}
|
||||||
CoinDesk.id(): CoinDesk,
|
|
||||||
CoinMarketCap.id(): CoinMarketCap,
|
|
||||||
ECB.id(): ECB
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,52 +4,54 @@ import requests
|
||||||
|
|
||||||
from pricehist.price import Price
|
from pricehist.price import Price
|
||||||
|
|
||||||
class CoinDesk():
|
|
||||||
|
|
||||||
|
class CoinDesk:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def id():
|
def id():
|
||||||
return 'coindesk'
|
return "coindesk"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name():
|
def name():
|
||||||
return 'CoinDesk Bitcoin Price Index'
|
return "CoinDesk Bitcoin Price Index"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def description():
|
def description():
|
||||||
return 'An average of bitcoin prices across leading global exchanges. Powered by CoinDesk, https://www.coindesk.com/price/bitcoin'
|
return "An average of bitcoin prices across leading global exchanges. Powered by CoinDesk, https://www.coindesk.com/price/bitcoin"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def source_url():
|
def source_url():
|
||||||
return 'https://www.coindesk.com/coindesk-api'
|
return "https://www.coindesk.com/coindesk-api"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bases():
|
def bases():
|
||||||
return ['BTC']
|
return ["BTC"]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def quotes():
|
def quotes():
|
||||||
url = 'https://api.coindesk.com/v1/bpi/supported-currencies.json'
|
url = "https://api.coindesk.com/v1/bpi/supported-currencies.json"
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
symbols = sorted([item['currency'] for item in data])
|
symbols = sorted([item["currency"] for item in data])
|
||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
def fetch(self, pair, start, end):
|
def fetch(self, pair, start, end):
|
||||||
base, quote = pair.split('/')
|
base, quote = pair.split("/")
|
||||||
if base not in self.bases():
|
if base not in self.bases():
|
||||||
exit(f'Invalid base {base}')
|
exit(f"Invalid base {base}")
|
||||||
if quote not in self.quotes():
|
if quote not in self.quotes():
|
||||||
exit(f'Invalid quote {quote}')
|
exit(f"Invalid quote {quote}")
|
||||||
|
|
||||||
min_start = '2010-07-17'
|
min_start = "2010-07-17"
|
||||||
if start < min_start:
|
if start < min_start:
|
||||||
exit(f'start {start} too early. The CoinDesk BPI only covers data from {min_start} onwards.')
|
exit(
|
||||||
|
f"start {start} too early. The CoinDesk BPI only covers data from {min_start} onwards."
|
||||||
|
)
|
||||||
|
|
||||||
url = f'https://api.coindesk.com/v1/bpi/historical/close.json?currency={quote}&start={start}&end={end}'
|
url = f"https://api.coindesk.com/v1/bpi/historical/close.json?currency={quote}&start={start}&end={end}"
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
prices = []
|
prices = []
|
||||||
for (d, v) in data['bpi'].items():
|
for (d, v) in data["bpi"].items():
|
||||||
prices.append(Price(base, quote, d, Decimal(str(v))))
|
prices.append(Price(base, quote, d, Decimal(str(v))))
|
||||||
|
|
||||||
return prices
|
return prices
|
||||||
|
|
|
@ -6,15 +6,15 @@ from xml.etree import ElementTree
|
||||||
|
|
||||||
from pricehist.price import Price
|
from pricehist.price import Price
|
||||||
|
|
||||||
class CoinMarketCap():
|
|
||||||
|
|
||||||
|
class CoinMarketCap:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def id():
|
def id():
|
||||||
return 'coinmarketcap'
|
return "coinmarketcap"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name():
|
def name():
|
||||||
return 'CoinMarketCap'
|
return "CoinMarketCap"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def description():
|
def description():
|
||||||
|
@ -22,7 +22,7 @@ class CoinMarketCap():
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def source_url():
|
def source_url():
|
||||||
return 'https://coinmarketcap.com/'
|
return "https://coinmarketcap.com/"
|
||||||
|
|
||||||
# # currency metadata - these may max out at 5k items (crypto data is currently 4720 items)
|
# # currency metadata - these may max out at 5k items (crypto data is currently 4720 items)
|
||||||
# curl 'https://web-api.coinmarketcap.com/v1/fiat/map?include_metals=true' | jq . | tee fiat-map.json
|
# curl 'https://web-api.coinmarketcap.com/v1/fiat/map?include_metals=true' | jq . | tee fiat-map.json
|
||||||
|
@ -37,24 +37,25 @@ class CoinMarketCap():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def fetch(self, pair, start, end):
|
def fetch(self, pair, start, end):
|
||||||
base, quote = pair.split('/')
|
base, quote = pair.split("/")
|
||||||
|
|
||||||
url = f'https://web-api.coinmarketcap.com/v1/cryptocurrency/ohlcv/historical'
|
url = f"https://web-api.coinmarketcap.com/v1/cryptocurrency/ohlcv/historical"
|
||||||
params = {
|
params = {
|
||||||
'symbol': base,
|
"symbol": base,
|
||||||
'convert': quote,
|
"convert": quote,
|
||||||
'time_start': int(datetime.strptime(start, '%Y-%m-%d').timestamp()),
|
"time_start": int(datetime.strptime(start, "%Y-%m-%d").timestamp()),
|
||||||
'time_end': int(datetime.strptime(end, '%Y-%m-%d').timestamp()) + 24*60*60 # round up to include the last day
|
"time_end": int(datetime.strptime(end, "%Y-%m-%d").timestamp())
|
||||||
|
+ 24 * 60 * 60, # round up to include the last day
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(url, params=params)
|
response = requests.get(url, params=params)
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
|
|
||||||
prices = []
|
prices = []
|
||||||
for item in data['data']['quotes']:
|
for item in data["data"]["quotes"]:
|
||||||
d = item['time_open'][0:10]
|
d = item["time_open"][0:10]
|
||||||
high = Decimal(str(item['quote'][quote]['high']))
|
high = Decimal(str(item["quote"][quote]["high"]))
|
||||||
low = Decimal(str(item['quote'][quote]['low']))
|
low = Decimal(str(item["quote"][quote]["low"]))
|
||||||
mid = sum([high, low]) / 2
|
mid = sum([high, low]) / 2
|
||||||
prices.append(Price(base, quote, d, mid))
|
prices.append(Price(base, quote, d, mid))
|
||||||
|
|
||||||
|
|
|
@ -6,51 +6,81 @@ from xml.etree import ElementTree
|
||||||
|
|
||||||
from pricehist.price import Price
|
from pricehist.price import Price
|
||||||
|
|
||||||
class ECB():
|
|
||||||
|
|
||||||
|
class ECB:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def id():
|
def id():
|
||||||
return 'ecb'
|
return "ecb"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name():
|
def name():
|
||||||
return 'European Central Bank'
|
return "European Central Bank"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def description():
|
def description():
|
||||||
return 'European Central Bank Euro foreign exchange reference rates'
|
return "European Central Bank Euro foreign exchange reference rates"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def source_url():
|
def source_url():
|
||||||
return 'https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html'
|
return "https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bases():
|
def bases():
|
||||||
return ['EUR']
|
return ["EUR"]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def quotes():
|
def quotes():
|
||||||
return ['AUD', 'BGN', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'GBP',
|
return [
|
||||||
'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'JPY', 'KRW',
|
"AUD",
|
||||||
'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK',
|
"BGN",
|
||||||
'SGD', 'THB', 'TRY', 'USD', 'ZAR']
|
"BRL",
|
||||||
|
"CAD",
|
||||||
|
"CHF",
|
||||||
|
"CNY",
|
||||||
|
"CZK",
|
||||||
|
"DKK",
|
||||||
|
"GBP",
|
||||||
|
"HKD",
|
||||||
|
"HRK",
|
||||||
|
"HUF",
|
||||||
|
"IDR",
|
||||||
|
"ILS",
|
||||||
|
"INR",
|
||||||
|
"ISK",
|
||||||
|
"JPY",
|
||||||
|
"KRW",
|
||||||
|
"MXN",
|
||||||
|
"MYR",
|
||||||
|
"NOK",
|
||||||
|
"NZD",
|
||||||
|
"PHP",
|
||||||
|
"PLN",
|
||||||
|
"RON",
|
||||||
|
"RUB",
|
||||||
|
"SEK",
|
||||||
|
"SGD",
|
||||||
|
"THB",
|
||||||
|
"TRY",
|
||||||
|
"USD",
|
||||||
|
"ZAR",
|
||||||
|
]
|
||||||
|
|
||||||
def fetch(self, pair, start, end):
|
def fetch(self, pair, start, end):
|
||||||
base, quote = pair.split('/')
|
base, quote = pair.split("/")
|
||||||
if base not in self.bases():
|
if base not in self.bases():
|
||||||
exit(f'Invalid base {base}')
|
exit(f"Invalid base {base}")
|
||||||
if quote not in self.quotes():
|
if quote not in self.quotes():
|
||||||
exit(f'Invalid quote {quote}')
|
exit(f"Invalid quote {quote}")
|
||||||
|
|
||||||
min_start = '1999-01-04'
|
min_start = "1999-01-04"
|
||||||
if start < min_start:
|
if start < min_start:
|
||||||
exit(f'start {start} too early. Minimum is {min_start}')
|
exit(f"start {start} too early. Minimum is {min_start}")
|
||||||
|
|
||||||
almost_90_days_ago = str(datetime.now().date() - timedelta(days=85))
|
almost_90_days_ago = str(datetime.now().date() - timedelta(days=85))
|
||||||
if start > almost_90_days_ago:
|
if start > almost_90_days_ago:
|
||||||
source_url = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml' # last 90 days
|
source_url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml" # last 90 days
|
||||||
else:
|
else:
|
||||||
source_url = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml' # since 1999
|
source_url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml" # since 1999
|
||||||
|
|
||||||
response = requests.get(source_url)
|
response = requests.get(source_url)
|
||||||
data = response.content
|
data = response.content
|
||||||
|
@ -58,16 +88,18 @@ class ECB():
|
||||||
# TODO consider changing from xml.etree to lxml
|
# TODO consider changing from xml.etree to lxml
|
||||||
root = ElementTree.fromstring(data)
|
root = ElementTree.fromstring(data)
|
||||||
namespaces = {
|
namespaces = {
|
||||||
'default': 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref',
|
"default": "http://www.ecb.int/vocabulary/2002-08-01/eurofxref",
|
||||||
'gesmes': 'http://www.gesmes.org/xml/2002-08-01'
|
"gesmes": "http://www.gesmes.org/xml/2002-08-01",
|
||||||
}
|
}
|
||||||
all_rows = []
|
all_rows = []
|
||||||
for day in root.find('default:Cube', namespaces):
|
for day in root.find("default:Cube", namespaces):
|
||||||
date = day.attrib['time']
|
date = day.attrib["time"]
|
||||||
rate_xpath = f"./*[@currency='{quote}']"
|
rate_xpath = f"./*[@currency='{quote}']"
|
||||||
# TODO what if it's not found for that day? (some quotes aren't in the earliest data)
|
# TODO what if it's not found for that day? (some quotes aren't in the earliest data)
|
||||||
rate = Decimal(day.find(rate_xpath).attrib['rate'])
|
rate = Decimal(day.find(rate_xpath).attrib["rate"])
|
||||||
all_rows.insert(0, (date, rate))
|
all_rows.insert(0, (date, rate))
|
||||||
selected = [ Price(base, quote, d, r) for d, r in all_rows if d >= start and d <= end ]
|
selected = [
|
||||||
|
Price(base, quote, d, r) for d, r in all_rows if d >= start and d <= end
|
||||||
|
]
|
||||||
|
|
||||||
return selected
|
return selected
|
||||||
|
|
|
@ -2,4 +2,4 @@ from pricehist import __version__
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
def test_version():
|
||||||
assert __version__ == '0.1.0'
|
assert __version__ == "0.1.0"
|
||||||
|
|
Loading…
Add table
Reference in a new issue