You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wowlet-backend/wowlet_backend/tasks/historical_prices.py

125 lines
4.0 KiB

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2020, The Monero Project.
# Copyright (c) 2020, dsc@xmr.pm
import asyncio
import os
import json
from typing import List, Union
from datetime import datetime
import aiofiles
from quart import current_app as app
import settings
from wowlet_backend.utils import httpget
from wowlet_backend.tasks import WowletTask
class HistoricalPriceTask(WowletTask):
"""
This class manages a historical price (USD) database, saved in a
textfile at `self._path`. A Feather wallet instance will ask
for the historical fiat price database on startup (but only
in chunks of a month for anti-fingerprinting reasons).
The task in this class simply keeps the fiat database
up-to-date locally.
"""
def __init__(self, interval: int = 43200):
super(HistoricalPriceTask, self).__init__(interval)
self._cache_key = f"historical_fiat"
self._path = f"data/historical_prices_{settings.COIN_SYMBOL}.json"
self._http_endpoint = f"https://www.coingecko.com/price_charts/{settings.COIN_NAME}/usd/max.json"
self._year_genesis = int(settings.COIN_GENESIS_DATE[:4])
asyncio.create_task(self._load())
async def task(self) -> Union[dict, None]:
content = await httpget(self._http_endpoint, json=True, raise_for_status=False)
if "stats" not in content:
raise Exception()
stats: List[List] = content.get('stats', []) # [[timestamp,USD],]
if not stats:
return
data = {
year: {
month: {} for month in range(1, 13)
} for year in range(self._year_genesis, datetime.now().year + 1)
}
# timestamp:USD
daily_price_blob = {day[0]: day[1] for day in stats}
# normalize
for timestamp, usd in daily_price_blob.items():
_date = datetime.fromtimestamp(timestamp / 1000)
data[_date.year].setdefault(_date.month, {})
data[_date.year][_date.month][_date.day] = usd
# update local database
await self._write(data)
return data
async def _load(self) -> None:
if not os.path.exists(self._path):
return
async with aiofiles.open(self._path, mode="r") as f:
content = await f.read()
blob = json.loads(content)
# ¯\_(ツ)_/¯
blob = {int(k): {
int(_k): {
int(__k): __v for __k, __v in _v.items()
} for _k, _v in v.items()
} for k, v in blob.items()}
await self.cache_set(self._cache_key, blob)
async def _write(self, blob: dict) -> None:
data = json.dumps(blob, sort_keys=True, indent=4)
async with aiofiles.open(self._path, mode="w") as f:
await f.write(data)
@staticmethod
async def get(year: int, month: int = None) -> Union[dict, None]:
"""This function is called when a Feather wallet client asks
for (a range of) historical fiat information. It returns the
data filtered by the parameters."""
from wowlet_backend.factory import cache
# redis keys are always strings, convert input parameters
year = str(year)
if isinstance(month, int):
month = str(month)
if not year.isnumeric() or (month and not month.isnumeric()):
app.logger.error(f"get() called with year: {year}, month {month}, not a number")
return
blob = await cache.get("historical_fiat")
blob = json.loads(blob)
if year not in blob:
return
rtn = {}
if not month:
for _m, days in blob[year].items():
for day, price in days.items():
rtn[datetime(int(year), int(_m), int(day)).strftime('%Y%m%d')] = price
return rtn
if month not in blob[year]:
return
for day, price in blob[year][month].items():
rtn[datetime(int(year), int(month), int(day)).strftime('%Y%m%d')] = price
return rtn