From 54595d9e5491c373ca3d67c18951463b1ff6b3dc Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Wed, 12 Jun 2019 21:31:06 +0200 Subject: [PATCH] source --- Background.py | 37 +++++++++++++++++++++++++++++++ Commands.py | 41 +++++++++++++++++++++++++++++++++++ LimitedDict.py | 38 ++++++++++++++++++++++++++++++++ LimitedList.py | 33 ++++++++++++++++++++++++++++ Market.py | 40 ++++++++++++++++++++++++++++++++++ Mercatus.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ Utils.py | 17 +++++++++++++++ 7 files changed, 265 insertions(+) create mode 100644 Background.py create mode 100644 Commands.py create mode 100644 LimitedDict.py create mode 100644 LimitedList.py create mode 100644 Market.py create mode 100644 Mercatus.py create mode 100644 Utils.py diff --git a/Background.py b/Background.py new file mode 100644 index 0000000..cb34719 --- /dev/null +++ b/Background.py @@ -0,0 +1,37 @@ +import asyncio +import threading + + +def interval(every: float or int, autorun=False, iterations=-1, isolated=False, *args_root, **kwargs_root): + def wrapper(fn): + + async def decorator(*args, **kwargs): + it = 0 + first = True + while iterations == -1 or it < iterations: + it += 1 + + if first: + first = False + else: + await asyncio.sleep(every) + + await fn(*args, **kwargs) + + def capsule(*args, **kwargs): + def loop_in_thread(l): + asyncio.set_event_loop(l) + l.run_until_complete(decorator(*args, **kwargs)) + + loop = asyncio.get_event_loop() + threading.Thread(target=loop_in_thread, args=(loop,)).start() + + if autorun: + if isolated: + capsule(*args_root, **kwargs_root) + else: + asyncio.run(decorator(*args_root, **kwargs_root)) + else: + return capsule if isolated else decorator + + return wrapper diff --git a/Commands.py b/Commands.py new file mode 100644 index 0000000..8a8c9d7 --- /dev/null +++ b/Commands.py @@ -0,0 +1,41 @@ +from telegram.ext import CallbackContext +from telegram import Update + +from LimitedList import LimitedList +from Utils import parse_command, config, Section + + +def get_watchlist(context: CallbackContext) -> LimitedList: + return LimitedList(context.user_data.setdefault(Section.Watchlist.value, []), + config[Section.Watchlist.value]['max_items']) + + +def watchlist_add(update: Update, context: CallbackContext): + value, *rest = parse_command(update) + get_watchlist(context).add(value) + update.message.reply_text('Saved ๐Ÿ’พ') + + +def watchlist_delete(update: Update, context: CallbackContext): + value, *rest = parse_command(update) + update.message.reply_text('Deleted ๐Ÿ—‘' if get_watchlist(context).delete(value) else 'Not found โ“') + + +def watchlist_all(update: Update, context: CallbackContext): + items = get_watchlist(context).all() + update.message.reply_text('\n'.join(items)) + + +def watchlist_clear(update: Update, context: CallbackContext): + get_watchlist(context).clear() + update.message.reply_text('Cleared ๐Ÿงผ') + + +def set_api_key(update: Update, context: CallbackContext): + value, *rest = parse_command(update) + context.user_data[Section.API_Key.value] = value + update.message.reply_text('API key saved ๐Ÿ”‘') + + +def get_api_key(update: Update, context: CallbackContext): + update.message.reply_text(context.user_data.get(Section.API_Key.value, 'API Key not set')) diff --git a/LimitedDict.py b/LimitedDict.py new file mode 100644 index 0000000..429a744 --- /dev/null +++ b/LimitedDict.py @@ -0,0 +1,38 @@ +import time +import math + + +class LimitedDict: + + def __init__(self, init: dict, limit: int): + self.dict = init + self.limit = limit + + def set(self, key, value): + # Delete oldest element if there are too many + if len(self.dict) + 1 > self.limit: + timestamp = math.inf + who = None + for cur, item in self.dict.items(): + if item['when'] < timestamp: + timestamp = item['when'] + who = cur + del self.dict[who] + + self.dict[key] = { + 'value': value, + 'when': int(time.time()) + } + + def get(self, key): + value = self.dict.get(key, None) + return value['value'] if value else None + + def delete(self, key): + self.dict.pop(key, None) + + def clear(self): + self.dict.clear() + + def all(self): + return [x['value'] for x in self.dict.values()] diff --git a/LimitedList.py b/LimitedList.py new file mode 100644 index 0000000..1c33107 --- /dev/null +++ b/LimitedList.py @@ -0,0 +1,33 @@ +from typing import List + + +class LimitedList: + + def __init__(self, init: List[str], limit: int): + self.data = init + self.limit = limit + + def _is_index(self, i: int) -> bool: + return False if i < 0 or i > len(self.data) - 1 else True + + def add(self, value: str): + # Delete oldest element if there are too many + if len(self.data) + 1 > self.limit: + self.data = self.data[1:] + + self.data.append(value) + + def get(self, i: int): + return self.data[i] if self._is_index(i) else None + + def delete(self, value: str): + if value in self.data: + self.data.remove(value) + return True + return False + + def clear(self): + self.data = [] + + def all(self): + return self.data diff --git a/Market.py b/Market.py new file mode 100644 index 0000000..33084cc --- /dev/null +++ b/Market.py @@ -0,0 +1,40 @@ +import io +from typing import BinaryIO +from alpha_vantage.techindicators import TechIndicators +from alpha_vantage.timeseries import TimeSeries +import matplotlib.pyplot as plt +import pandas as pd +from datetime import datetime + + +class Market: + + def __init__(self, key: str): + self.ti = TechIndicators(key=key, output_format='pandas') + self.ts = TimeSeries(key=key, output_format='pandas') + + def get_wma(self, stock: str, since: datetime) -> BinaryIO: + plt.clf() + fig = plt.figure() + ax = plt.gca() + ax.set_ylabel('Dollar') + + def sort_data_and_plot(data: pd.DataFrame, x: str, y: str, label: str): + data = data.reset_index() + data['date'] = data['date'].astype('datetime64[ns]') + data = data.loc[since < data['date']] + data.plot(x=x, y=y, ax=ax, label=label) + + df: pd.DataFrame = self.ts.get_daily(symbol=stock, outputsize='full')[0] + sort_data_and_plot(data=df, x='date', y='1. open', label='Value') + + for period in [45, 90, 120]: + df: pd.DataFrame = self.ti.get_wma(symbol=stock, interval='daily', time_period=period)[0] + sort_data_and_plot(data=df, x='date', y='WMA', label=str(period)) + + plt.plot() + # plt.show() + buffer = io.BytesIO() + fig.savefig(buffer, format='png') + buffer.seek(0) + return buffer diff --git a/Mercatus.py b/Mercatus.py new file mode 100644 index 0000000..caa0106 --- /dev/null +++ b/Mercatus.py @@ -0,0 +1,59 @@ +from asyncio import sleep +from datetime import datetime, timedelta +from telegram.ext import Updater, CommandHandler + +from Background import interval +from Market import Market +from Utils import persistence, config, Section +from Commands import watchlist_add, watchlist_delete, watchlist_all, watchlist_clear, set_api_key, get_api_key + +updater: Updater = Updater(config['token'], use_context=True, persistence=persistence) + + +@interval(every=1.0 * 60 * 60, autorun=False, isolated=True) +async def send_updates(): + delta = datetime.now() - timedelta(days=365 * 1) + + for key, data in persistence.get_user_data().items(): + if Section.API_Key.value not in data: + continue + + market = Market(data[Section.API_Key.value]) + updater.bot.send_message(key, text='Getting updates ๐ŸŒŽ') + first = True + for item in data.get(Section.Watchlist.value, []): + if first: + first = False + else: + msg = updater.bot.send_message(key, text='Waiting 60 seconds for API... โณ') + await sleep(60) + msg.delete() + + msg = updater.bot.send_message(key, text='Calculating... โณ') + updater.bot.send_photo(key, photo=market.get_wma(item, delta)) + msg.delete() + updater.bot.send_message(key, text=item) + + # Repeat after every hour + # threading.Timer(1.0 * 60 * 60, send_updates).start() + + +def main(): + global updater + dp = updater.dispatcher + + dp.add_handler(CommandHandler('add', watchlist_add)) + dp.add_handler(CommandHandler('delete', watchlist_delete)) + dp.add_handler(CommandHandler('list', watchlist_all)) + dp.add_handler(CommandHandler('clear', watchlist_clear)) + dp.add_handler(CommandHandler('setKey', set_api_key)) + dp.add_handler(CommandHandler('getKey', get_api_key)) + + print('Started ๐Ÿš€') + send_updates() + updater.start_polling() + updater.idle() + + +if __name__ == '__main__': + main() diff --git a/Utils.py b/Utils.py new file mode 100644 index 0000000..3703897 --- /dev/null +++ b/Utils.py @@ -0,0 +1,17 @@ +from telegram import Update +from telegram.ext import PicklePersistence +from yaml import load, Loader +from enum import Enum + +config = load(open('./config.yml', 'r'), Loader=Loader) +persistence = PicklePersistence('./data/mercatus') + + +class Section(Enum): + Watchlist = 'watchlist' + API_Key = 'api_key' + + +def parse_command(update: Update) -> (str, str): + key, value = (update.message.text.split(' ', 1)[1].split(' ', 1) + [None])[:2] + return key, value