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:
2024-11-30 03:27:32 -08:00
parent c1a3c73c1d
commit c78ced85ca
35 changed files with 1184 additions and 0 deletions

View File

View 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()

View 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]

View 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