Very Initial MVP
There is so much more to do, but I think it is time to commit this to VCS
This commit is contained in:
0
token_bot/history_manager/__init__.py
Normal file
0
token_bot/history_manager/__init__.py
Normal file
55
token_bot/history_manager/history.py
Normal file
55
token_bot/history_manager/history.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import datetime
|
||||
from typing import LiteralString, Set, List, Tuple
|
||||
|
||||
from token_bot.history_manager.update_trigger import UpdateTrigger
|
||||
from token_bot.persistant_database import AlertType, Alert
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
|
||||
class History:
|
||||
def __init__(self, flavor: Flavor, region: Region):
|
||||
self._flavor : Flavor = flavor
|
||||
self._region : Region = region
|
||||
self._history : List[Tuple[datetime, int]] = []
|
||||
self._last_price_movement : int = 0
|
||||
self._latest_price_datum : Tuple[datetime.datetime, int] | None = None
|
||||
self._update_triggers : List[UpdateTrigger] = []
|
||||
self._squelched_alerts : List[Alert] = []
|
||||
|
||||
for alert_type in AlertType:
|
||||
self._update_triggers.append(UpdateTrigger(Alert(alert_type, flavor, self._region)))
|
||||
|
||||
@property
|
||||
def flavor(self) -> Flavor:
|
||||
return self._flavor
|
||||
|
||||
@property
|
||||
def region(self) -> Region:
|
||||
return self._region
|
||||
|
||||
@property
|
||||
def last_price_datum(self) -> Tuple[datetime.datetime, int] | None:
|
||||
return self._latest_price_datum
|
||||
|
||||
@property
|
||||
def history(self) -> List[Tuple[datetime.datetime, int]]:
|
||||
return self._history
|
||||
|
||||
async def _process_update_triggers(self) -> List[Alert]:
|
||||
alerts = []
|
||||
for trigger in self._update_triggers:
|
||||
if trigger.check_and_update(self._latest_price_datum, self._history) and trigger.alert not in self._squelched_alerts:
|
||||
alerts.append(trigger.alert)
|
||||
|
||||
return alerts
|
||||
|
||||
async def add_price(self, datum: Tuple[datetime.datetime, int]) -> List[Alert]:
|
||||
if self._latest_price_datum is None:
|
||||
self._latest_price_datum = datum
|
||||
self._last_price_movement = datum[1] - self._latest_price_datum[1]
|
||||
self._latest_price_datum = datum
|
||||
self._history.append(datum)
|
||||
return await self._process_update_triggers()
|
||||
|
||||
|
||||
61
token_bot/history_manager/history_manager.py
Normal file
61
token_bot/history_manager/history_manager.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import datetime
|
||||
from typing import Set, List, Dict, LiteralString, Tuple
|
||||
|
||||
from token_bot.history_manager.history import History
|
||||
from token_bot.persistant_database import Alert
|
||||
from token_bot.token_database.flavor import Flavor
|
||||
from token_bot.token_database.region import Region
|
||||
|
||||
from token_bot.token_database import database as tdb
|
||||
|
||||
class HistoryManager:
|
||||
def __init__(self, token_db: tdb.Database):
|
||||
self._history : Dict[Flavor, Dict[Region, History]] = {}
|
||||
self._tdb : tdb.Database = token_db
|
||||
for flavor in Flavor:
|
||||
self._history[flavor] = {}
|
||||
for region in Region:
|
||||
self._history[flavor][Region(region)] = History(flavor, Region(region))
|
||||
|
||||
|
||||
async def _retrieve_data(self, flavor: Flavor, region: Region) -> List[Tuple[datetime.datetime, int]]:
|
||||
high_fidelity_time = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(hours=72)
|
||||
all_history = await self._tdb.history(flavor, region)
|
||||
high_fidelity_history = await self._tdb.history(flavor, region, '72h')
|
||||
final_response = []
|
||||
for data_point in all_history:
|
||||
datum = (datetime.datetime.fromisoformat(data_point[0]), data_point[1])
|
||||
if datum[0] < high_fidelity_time:
|
||||
final_response.append(datum)
|
||||
for data_point in high_fidelity_history:
|
||||
datum = (datetime.datetime.fromisoformat(data_point[0]), data_point[1])
|
||||
final_response.append(datum)
|
||||
return final_response
|
||||
|
||||
|
||||
async def load_data(self):
|
||||
for flavor in Flavor:
|
||||
for r in Region:
|
||||
region = Region(r)
|
||||
history = History(flavor, Region(region))
|
||||
history_response = await self._retrieve_data(flavor, region)
|
||||
for item in history_response:
|
||||
await history.add_price(item)
|
||||
self._history[flavor][region] = history
|
||||
|
||||
|
||||
async def update_data(self, flavor: Flavor, region: Region) -> List[Alert]:
|
||||
history = self._history[flavor][region]
|
||||
current_price_data = await self._tdb.current(flavor)
|
||||
current_region_data = current_price_data[region.value.lower()]
|
||||
datum = (
|
||||
datetime.datetime.fromisoformat(current_region_data[0]),
|
||||
current_region_data[1]
|
||||
)
|
||||
if datum != history.last_price_datum:
|
||||
return await history.add_price(datum)
|
||||
return []
|
||||
|
||||
|
||||
async def get_history(self, flavor, region) -> History:
|
||||
return self._history[flavor][region]
|
||||
79
token_bot/history_manager/update_trigger.py
Normal file
79
token_bot/history_manager/update_trigger.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import datetime
|
||||
import operator
|
||||
from typing import Tuple, List
|
||||
|
||||
from token_bot.persistant_database import Alert, AlertType
|
||||
|
||||
|
||||
class UpdateTrigger:
|
||||
def __init__(self, alert: Alert):
|
||||
self._alert : Alert = alert
|
||||
self._last_trigger : Tuple[datetime.datetime, int] | None = None
|
||||
|
||||
@property
|
||||
def alert(self) -> Alert:
|
||||
return self._alert
|
||||
|
||||
def _find_next_trigger(self, comparison_operator: operator, starting_point: datetime.datetime, history: List[Tuple[datetime.datetime, int]]):
|
||||
candidate_datum : Tuple[datetime.datetime, int] | None = None
|
||||
for datum in history:
|
||||
if candidate_datum is None:
|
||||
candidate_datum = datum
|
||||
if (datum[0] > starting_point and datum != history[-1]) and comparison_operator(datum[1], candidate_datum[1]):
|
||||
candidate_datum = datum
|
||||
self._last_trigger = candidate_datum
|
||||
|
||||
def check_and_update(self, new_datum: Tuple[datetime.datetime, int], history: List[Tuple[datetime.datetime, int]]) -> bool:
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
match self._alert.alert_type:
|
||||
case AlertType.DAILY_LOW:
|
||||
time_range = datetime.timedelta(days=1)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.DAILY_HIGH:
|
||||
time_range = datetime.timedelta(days=1)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.WEEKLY_LOW:
|
||||
time_range = datetime.timedelta(weeks=1)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.WEEKLY_HIGH:
|
||||
time_range = datetime.timedelta(weeks=1)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.MONTHLY_LOW:
|
||||
time_range = datetime.timedelta(days=31)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.MONTHLY_HIGH:
|
||||
time_range = datetime.timedelta(days=31)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.YEARLY_LOW:
|
||||
time_range = datetime.timedelta(days=365)
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.YEARLY_HIGH:
|
||||
time_range = datetime.timedelta(days=365)
|
||||
comparison_operator = operator.gt
|
||||
case AlertType.ALL_TIME_LOW:
|
||||
# TODO Make this calculate based on Flavor and current time the range back to look
|
||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||
comparison_operator = operator.lt
|
||||
case AlertType.ALL_TIME_HIGH:
|
||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||
comparison_operator = operator.gt
|
||||
case _:
|
||||
# TODO: The logic here is certainly wrong for Custom
|
||||
time_range = datetime.timedelta(days=int(365.25 * 6))
|
||||
comparison_operator = operator.eq
|
||||
|
||||
if new_datum[0] > now - time_range:
|
||||
if self._last_trigger is None:
|
||||
self._last_trigger = new_datum
|
||||
return True
|
||||
|
||||
# If the self._last_trigger falls out of scope of the alert, find the next thing that would have triggered
|
||||
# the alert so the next time a high or low comes up it's correctly comparing against the range of the alert
|
||||
# rather than since the alert triggered
|
||||
if self._last_trigger[0] < now - time_range:
|
||||
self._find_next_trigger(comparison_operator, now - time_range, history)
|
||||
|
||||
if comparison_operator(new_datum[1], self._last_trigger[1]):
|
||||
self._last_trigger = new_datum
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user