Add support for Yahoo! Finance.
This commit is contained in:
parent
925ed42b86
commit
10982b72d4
6 changed files with 133 additions and 15 deletions
|
@ -4,11 +4,10 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from pricehist import __version__, outputs, sources
|
from pricehist import __version__, logger, outputs, sources
|
||||||
from pricehist.fetch import fetch
|
from pricehist.fetch import fetch
|
||||||
from pricehist.format import Format
|
from pricehist.format import Format
|
||||||
from pricehist.series import Series
|
from pricehist.series import Series
|
||||||
from pricehist import logger
|
|
||||||
|
|
||||||
|
|
||||||
def cli(args=None, output_file=sys.stdout):
|
def cli(args=None, output_file=sys.stdout):
|
||||||
|
@ -34,7 +33,7 @@ def cli(args=None, output_file=sys.stdout):
|
||||||
print(result, file=output_file)
|
print(result, file=output_file)
|
||||||
elif args.command == "source" and args.symbols:
|
elif args.command == "source" and args.symbols:
|
||||||
result = sources.by_id[args.source].format_symbols()
|
result = sources.by_id[args.source].format_symbols()
|
||||||
print(result, file=output_file)
|
print(result, file=output_file, end="")
|
||||||
elif args.command == "source":
|
elif args.command == "source":
|
||||||
total_width = shutil.get_terminal_size().columns
|
total_width = shutil.get_terminal_size().columns
|
||||||
result = sources.by_id[args.source].format_info(total_width)
|
result = sources.by_id[args.source].format_info(total_width)
|
||||||
|
@ -72,9 +71,6 @@ def build_parser():
|
||||||
if base == "":
|
if base == "":
|
||||||
msg = f"No base found in the requested pair '{s}'."
|
msg = f"No base found in the requested pair '{s}'."
|
||||||
raise argparse.ArgumentTypeError(msg)
|
raise argparse.ArgumentTypeError(msg)
|
||||||
if quote == "":
|
|
||||||
msg = f"No quote found in the requested pair '{s}'."
|
|
||||||
raise argparse.ArgumentTypeError(msg)
|
|
||||||
return (base, quote)
|
return (base, quote)
|
||||||
|
|
||||||
def valid_date(s):
|
def valid_date(s):
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from importlib.resources import read_text
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from importlib.resources import read_text
|
||||||
|
|
||||||
from pricehist import __version__
|
from pricehist import __version__
|
||||||
from pricehist.format import Format
|
from pricehist.format import Format
|
||||||
|
@ -115,7 +114,7 @@ class GnuCashSQL(BaseOutput):
|
||||||
# - https://mariadb.com/kb/en/string-literals/
|
# - https://mariadb.com/kb/en/string-literals/
|
||||||
# - https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
|
# - https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
|
||||||
# - https://www.postgresql.org/docs/devel/sql-syntax-lexical.html
|
# - https://www.postgresql.org/docs/devel/sql-syntax-lexical.html
|
||||||
escaped = re.sub("'", "''", s)
|
escaped = s.replace("'", "''")
|
||||||
quoted = f"'{escaped}'"
|
quoted = f"'{escaped}'"
|
||||||
return quoted
|
return quoted
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from .coindesk import CoinDesk
|
from .coindesk import CoinDesk
|
||||||
from .coinmarketcap import CoinMarketCap
|
from .coinmarketcap import CoinMarketCap
|
||||||
from .ecb import ECB
|
from .ecb import ECB
|
||||||
|
from .yahoo import Yahoo
|
||||||
|
|
||||||
by_id = {source.id(): source for source in [CoinDesk(), CoinMarketCap(), ECB()]}
|
by_id = {
|
||||||
|
source.id(): source for source in [CoinDesk(), CoinMarketCap(), ECB(), Yahoo()]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def formatted():
|
def formatted():
|
||||||
|
|
|
@ -51,9 +51,9 @@ class BaseSource(ABC):
|
||||||
|
|
||||||
def format_symbols(self) -> str:
|
def format_symbols(self) -> str:
|
||||||
symbols = self.symbols()
|
symbols = self.symbols()
|
||||||
width = max([len(sym) for sym, desc in symbols])
|
width = max([len(sym) for sym, desc in symbols] + [0])
|
||||||
lines = [sym.ljust(width + 4) + desc for sym, desc in symbols]
|
lines = [sym.ljust(width + 4) + desc + "\n" for sym, desc in symbols]
|
||||||
return "\n".join(lines)
|
return "".join(lines)
|
||||||
|
|
||||||
def format_info(self, total_width=80) -> str:
|
def format_info(self, total_width=80) -> str:
|
||||||
k_width = 11
|
k_width = 11
|
||||||
|
@ -82,7 +82,7 @@ class BaseSource(ABC):
|
||||||
first, *rest = value.split("\n")
|
first, *rest = value.split("\n")
|
||||||
first_output = wrapper.wrap(first)
|
first_output = wrapper.wrap(first)
|
||||||
wrapper.initial_indent = subsequent_indent
|
wrapper.initial_indent = subsequent_indent
|
||||||
rest_output = sum([wrapper.wrap(line) if line else ["\n"] for line in rest], [])
|
rest_output = sum([wrapper.wrap(line) if line else [""] for line in rest], [])
|
||||||
output = "\n".join(first_output + rest_output)
|
output = "\n".join(first_output + rest_output)
|
||||||
if output != "":
|
if output != "":
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -77,7 +77,7 @@ class CoinMarketCap(BaseSource):
|
||||||
params["convert"] = series.quote
|
params["convert"] = series.quote
|
||||||
|
|
||||||
params["time_start"] = int(
|
params["time_start"] = int(
|
||||||
datetime.strptime(series.start, "%Y-%m-%d").timestamp()
|
int(datetime.strptime(series.start, "%Y-%m-%d").timestamp())
|
||||||
)
|
)
|
||||||
params["time_end"] = (
|
params["time_end"] = (
|
||||||
int(datetime.strptime(series.end, "%Y-%m-%d").timestamp()) + 24 * 60 * 60
|
int(datetime.strptime(series.end, "%Y-%m-%d").timestamp()) + 24 * 60 * 60
|
||||||
|
|
120
src/pricehist/sources/yahoo.py
Normal file
120
src/pricehist/sources/yahoo.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import csv
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from pricehist.price import Price
|
||||||
|
|
||||||
|
from .basesource import BaseSource
|
||||||
|
|
||||||
|
|
||||||
|
class Yahoo(BaseSource):
|
||||||
|
def id(self):
|
||||||
|
return "yahoo"
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "Yahoo! Finance"
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
return (
|
||||||
|
"Historical data for most Yahoo! Finance symbols, "
|
||||||
|
"as available on the web page"
|
||||||
|
)
|
||||||
|
|
||||||
|
def source_url(self):
|
||||||
|
return "https://finance.yahoo.com/"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
return "1970-01-01"
|
||||||
|
|
||||||
|
def types(self):
|
||||||
|
return ["adjclose", "open", "high", "low", "close", "mid"]
|
||||||
|
|
||||||
|
def notes(self):
|
||||||
|
return (
|
||||||
|
"Yahoo! Finance decommissioned its historical data API in 2017 but "
|
||||||
|
"some historical data is available via its web page, as described in: "
|
||||||
|
"https://help.yahoo.com/kb/"
|
||||||
|
"download-historical-data-yahoo-finance-sln2311.html\n"
|
||||||
|
f"{self._symbols_message()}\n"
|
||||||
|
"In output the base and quote will be the Yahoo! symbol and its "
|
||||||
|
"corresponding currency. Some symbols include the name of the quote "
|
||||||
|
"currency (e.g. BTC-USD), so you may wish to use --fmt-base to "
|
||||||
|
"remove the redundant information.\n"
|
||||||
|
"When a symbol's historical data is unavilable due to data licensing "
|
||||||
|
"restrictions, its web page will show no download button and "
|
||||||
|
"pricehist will only find the current day's price."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _symbols_message(self):
|
||||||
|
return (
|
||||||
|
"Find the symbol of interest on https://finance.yahoo.com/ and use "
|
||||||
|
"that as the PAIR in your pricehist command. Prices for each symbol "
|
||||||
|
"are given in its native currency."
|
||||||
|
)
|
||||||
|
|
||||||
|
def symbols(self):
|
||||||
|
logging.info(self._symbols_message())
|
||||||
|
return []
|
||||||
|
|
||||||
|
def fetch(self, series):
|
||||||
|
spark, history = self._data(series)
|
||||||
|
|
||||||
|
output_quote = spark["spark"]["result"][0]["response"][0]["meta"]["currency"]
|
||||||
|
|
||||||
|
prices = [
|
||||||
|
Price(row["date"], amount)
|
||||||
|
for row in history
|
||||||
|
if (amount := self._amount(row, series.type))
|
||||||
|
]
|
||||||
|
|
||||||
|
return dataclasses.replace(series, quote=output_quote, prices=prices)
|
||||||
|
|
||||||
|
def _amount(self, row, type):
|
||||||
|
if type != "mid" and row[type] != "null":
|
||||||
|
return Decimal(row[type])
|
||||||
|
elif type == "mid" and row["high"] != "null" and row["low"] != "null":
|
||||||
|
return sum([Decimal(row["high"]), Decimal(row["low"])]) / 2
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _data(self, series) -> (dict, csv.DictReader):
|
||||||
|
base_url = "https://query1.finance.yahoo.com/v7/finance"
|
||||||
|
|
||||||
|
spark_url = f"{base_url}/spark"
|
||||||
|
spark_params = {
|
||||||
|
"symbols": series.base,
|
||||||
|
"range": "1d",
|
||||||
|
"interval": "1d",
|
||||||
|
"indicators": "close",
|
||||||
|
"includeTimestamps": "false",
|
||||||
|
"includePrePost": "false",
|
||||||
|
}
|
||||||
|
spark_response = self.log_curl(requests.get(spark_url, params=spark_params))
|
||||||
|
spark = json.loads(spark_response.content)
|
||||||
|
|
||||||
|
start_ts = int(datetime.strptime(series.start, "%Y-%m-%d").timestamp())
|
||||||
|
end_ts = int(datetime.strptime(series.end, "%Y-%m-%d").timestamp()) + (
|
||||||
|
24 * 60 * 60
|
||||||
|
) # round up to include the last day
|
||||||
|
|
||||||
|
history_url = f"{base_url}/download/{series.base}"
|
||||||
|
history_params = {
|
||||||
|
"period1": start_ts,
|
||||||
|
"period2": end_ts,
|
||||||
|
"interval": "1d",
|
||||||
|
"events": "history",
|
||||||
|
"includeAdjustedClose": "true",
|
||||||
|
}
|
||||||
|
history_response = self.log_curl(
|
||||||
|
requests.get(history_url, params=history_params)
|
||||||
|
)
|
||||||
|
history_lines = history_response.content.decode("utf-8").splitlines()
|
||||||
|
history_lines[0] = history_lines[0].lower().replace(" ", "")
|
||||||
|
history = csv.DictReader(history_lines, delimiter=",")
|
||||||
|
|
||||||
|
return (spark, history)
|
Loading…
Add table
Reference in a new issue