major rewrite, use conversation handler for settings, user can set interval for getting notifications

This commit is contained in:
cupcakearmy 2019-12-17 14:31:12 +01:00
parent 224c7fafa7
commit 59d5210372
9 changed files with 340 additions and 148 deletions

View File

@ -1,112 +0,0 @@
import asyncio
from asyncio import sleep
from datetime import datetime, timedelta
from telegram.ext import CallbackContext
from telegram import Update, ParseMode
from Background import interval
from LimitedList import LimitedList
from Market import Market
from Utils import parse_command, config, Section, persistence, updater
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) if len(items) > 0 else 'Your list is empty 📭')
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 ⛔️'))
def data(update: Update, context: CallbackContext):
id = update.message.chat_id
delta = datetime.now() - timedelta(days=365 * 1)
asyncio.run(send_update_to_user(user=id, delta=delta))
def start(update: Update, context: CallbackContext):
update.message.reply_markdown("""*Welcome! 👋*
*1.* First you will need to get a (free) api token for the stock data.
[https://www.alphavantage.co/support/#api-key](Alphavantage Key 🔑)
*2.* Then enter it by sending the code to me with `/setKey myApiCode`
*3.* Add stocks or ETFs to your `/list` by going to [https://finance.yahoo.com/](Yahoo Finance 📈) and the sending it to `/add`
_Example_ For Apple `/add AAPL`
Enjoy 🚀
""")
async def send_update_to_user(user: str, delta: datetime):
try:
user_data = persistence.get_user_data()[user]
running = user_data.setdefault(Section.Running.value, False)
if Section.API_Key.value not in user_data:
updater.bot.send_message(user, text='API Key not set ⛔️')
return
if running:
updater.bot.send_message(user, text='Already running 🏃')
return
user_data[Section.Running.value] = True
print('Sending updates to {}'.format(user))
market = Market(user_data[Section.API_Key.value])
updater.bot.send_message(user, text='Getting updates 🌎')
first = True
for item in user_data.get(Section.Watchlist.value, []):
if first:
first = False
else:
msg = updater.bot.send_message(user, text='Waiting 60 seconds for API... ⏳')
await sleep(60)
msg.delete()
msg = updater.bot.send_message(user, text='Calculating {}... ⏳'.format(item))
chart = market.get_wma(item, delta)
msg.delete()
updater.bot.send_message(user, text='*{}*'.format(item), parse_mode=ParseMode.MARKDOWN)
updater.bot.send_photo(user, photo=chart)
except:
updater.bot.send_message(user, text='There was an error ⚠️')
finally:
user_data[Section.Running.value] = False
@interval(every=3 * 60 * 60.0, autorun=False, isolated=True)
async def send_updates():
delta = datetime.now() - timedelta(days=365 * 1)
for key in persistence.get_user_data().keys():
await send_update_to_user(user=key, delta=delta)

121
src/commands/config.py Normal file
View File

@ -0,0 +1,121 @@
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardRemove, ParseMode
from telegram.ext import CommandHandler, MessageHandler, Filters, ConversationHandler, CallbackQueryHandler, CallbackContext
from utils import Section
MENU, API_KEY, FREQUENCY = range(3)
def show_menu(update: Update, context: CallbackContext):
keyboard = [
[InlineKeyboardButton('API Key', callback_data=API_KEY)],
[InlineKeyboardButton('Frequency', callback_data=FREQUENCY)],
[InlineKeyboardButton('Done', callback_data=ConversationHandler.END)],
]
update.effective_user.send_message(
'_Current settings:_\n'
f'API Key: *{context.user_data[Section.API_Key.value]}*\n'
f'Frequency: *{context.user_data[Section.Frequency.value]}*\n'
'\nWhat settings do you want to configure?',
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(keyboard, one_time_keyboard=True)
)
return MENU
def show_menu_api_key(update: Update, context: CallbackContext):
update.effective_user.send_message(
'Send me your API Key 🙂'
'\nor /cancel',
reply_markup=ReplyKeyboardRemove()
)
return API_KEY
def show_menu_frequency(update: Update, context: CallbackContext):
keyboard = [
[InlineKeyboardButton('2 minutes', callback_data='2m'), InlineKeyboardButton(
'30 minutes', callback_data='30m')],
[InlineKeyboardButton('hour', callback_data='1h'), InlineKeyboardButton(
'4 hours', callback_data='4h')],
[InlineKeyboardButton('12 hours', callback_data='12h'), InlineKeyboardButton(
'day', callback_data='1d')],
[InlineKeyboardButton('3 days', callback_data='3d'), InlineKeyboardButton(
'week', callback_data='1w')],
[InlineKeyboardButton('Cancel', callback_data='cancel')],
]
update.effective_user.send_message(
'Send me updates every: ⬇',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return FREQUENCY
def config(update: Update, context: CallbackContext):
context.bot.delete_message(
chat_id=update.message.chat_id,
message_id=update.message.message_id,
)
return show_menu(update, context)
def menu(update: Update, context: CallbackContext):
selected = int(update.callback_query.data)
context.bot.delete_message(
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
)
if selected == API_KEY:
return show_menu_api_key(update, context)
elif selected == FREQUENCY:
return show_menu_frequency(update, context)
else:
return ConversationHandler.END
def set_api_key(update, context):
reply = update.message.text
context.user_data[Section.API_Key.value] = reply
update.message.reply_text(f'Saved {reply} 💾', reply_markup=ReplyKeyboardRemove())
return show_menu(update, context)
def set_frequency(update: Update, context: CallbackContext):
selected = update.callback_query.data
if selected != 'cancel':
update.callback_query.edit_message_text(f'Saved {selected} 💪')
context.user_data[Section.Frequency.value] = selected
else:
context.bot.delete_message(
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
)
return show_menu(update, context)
def cancel(update: Update, context: CallbackContext):
update.message.reply_text('Canceled', reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
config_handler = ConversationHandler(
entry_points=[CommandHandler('config', config)],
states={
MENU: [CallbackQueryHandler(menu)],
API_KEY: [
CommandHandler('cancel', cancel),
MessageHandler(Filters.all, set_api_key),
],
FREQUENCY: [CallbackQueryHandler(set_frequency)],
},
fallbacks=[CommandHandler('cancel', cancel)]
)

94
src/commands/other.py Normal file
View File

@ -0,0 +1,94 @@
from asyncio import sleep, run
from datetime import datetime
from threading import Timer
from pytimeparse import parse
from telegram import Update, ParseMode
from telegram.ext import CallbackContext
from telegram.ext.dispatcher import run_async
from market import Market
from text import INTRO_TEXT
from utils import Section, persistence, updater, current_timestamp, delta_timestamp
SENDING = False
def error(update: Update, context: CallbackContext):
print(context.error)
def start(update: Update, context: CallbackContext):
update.message.reply_markdown(INTRO_TEXT)
@run_async
def data(update: Update, context: CallbackContext):
delta = current_timestamp() - context.user_data.setdefault(Section.Interval.value, delta_timestamp(days=365))
send_update_to_user(user=update.effective_user['id'], delta=delta)
def send_update_to_user(user: str, delta: int):
user_data = None
try:
user_data = persistence.user_data[user]
running = user_data.setdefault(Section.Running.value, False)
print(f'Running {user} - {user_data}')
if Section.API_Key.value not in user_data:
updater.bot.send_message(user, text='API Key not set ⛔️')
return
if running:
updater.bot.send_message(user, text='Already running 🏃')
return
print(f'Sending updates to {user}')
user_data[Section.Running.value] = True
user_data[Section.LastRun.value] = current_timestamp()
market = Market(user_data[Section.API_Key.value])
updater.bot.send_message(user, text='Getting updates 🌎')
first = True
for item in user_data.get(Section.Watchlist.value, []):
if first:
first = False
else:
# Wait to not overload the api
msg = updater.bot.send_message(user, text='Waiting 60 seconds for API... ⏳')
run(sleep(60))
msg.delete()
msg = updater.bot.send_message(user, text=f'Calculating {item}... ⏳')
chart = market.get_wma(item, datetime.fromtimestamp(delta))
msg.delete()
updater.bot.send_photo(user, photo=chart, caption=f'*{item}*',
parse_mode=ParseMode.MARKDOWN, disable_notification=True)
except Exception as e:
print(f'{user} - {e}')
updater.bot.send_message(user, text=f'There was an error ⚠️\n {e}')
finally:
if user_data:
user_data[Section.Running.value] = False
def send_updates(context: CallbackContext):
global SENDING
try:
if SENDING:
return
SENDING = True
now = current_timestamp()
for user, user_data in persistence.user_data.items():
last_run = user_data.setdefault(Section.LastRun.value, 0)
frequency = parse(user_data.setdefault(Section.Frequency.value, '1d'))
if last_run + frequency < now:
delta = now - user_data.setdefault(Section.Interval.value, delta_timestamp(days=365))
send_update_to_user(user=user, delta=delta)
finally:
SENDING = False

45
src/commands/watchlist.py Normal file
View File

@ -0,0 +1,45 @@
from telegram import Update
from telegram.ext import CallbackContext
from limited_list import LimitedList
from utils import parse_command, config, Section
def get_watchlist(context: CallbackContext) -> LimitedList:
return LimitedList(
config[Section.Watchlist.value]['max_items'],
context.user_data.setdefault(Section.Watchlist.value, []),
)
def save_watchlist(context: CallbackContext, l: LimitedList):
context.user_data[Section.Watchlist.value] = l.all()
def watchlist_add(update: Update, context: CallbackContext):
value, *rest = parse_command(update)
wl = get_watchlist(context)
wl.add(str(value).upper())
save_watchlist(context, wl)
update.message.reply_text('Saved 💾')
def watchlist_delete(update: Update, context: CallbackContext):
value, *rest = parse_command(update)
wl = get_watchlist(context)
found = wl.delete(value)
save_watchlist(context, wl)
update.message.reply_text('Deleted 🗑' if found else 'Not found ❓')
def watchlist_all(update: Update, context: CallbackContext):
items = get_watchlist(context).all()
update.message.reply_text('\n'.join(items) if len(items) > 0 else 'Your list is empty 📭')
def watchlist_clear(update: Update, context: CallbackContext):
wl = get_watchlist(context)
wl.clear()
save_watchlist(context, wl)
update.message.reply_text('Cleared 🧼')

View File

@ -1,21 +1,27 @@
from typing import List from typing import List, Optional
class LimitedList: class LimitedList:
"""
Basically a List that has a maximum amount of entries.
When full and new elements get appended the oldest gets deleted.
"""
def __init__(self, init: List[str], limit: int): def __init__(self, limit: int, init: Optional[List[str]]):
self.data = init self.data = init if init else []
self.limit = limit self.limit = limit
def _is_index(self, i: int) -> bool: def _is_index(self, i: int) -> bool:
return False if i < 0 or i > len(self.data) - 1 else True return False if i < 0 or i > len(self.data) - 1 else True
def add(self, value: str): def add(self, value: str):
print(f'Before {self.data}')
# Delete oldest element if there are too many # Delete oldest element if there are too many
if len(self.data) + 1 > self.limit: if len(self.data) + 1 > self.limit:
self.data = self.data[1:] self.data = self.data[1:]
self.data.append(value) self.data.append(value)
print(f'After {self.data}')
def get(self, i: int): def get(self, i: int):
return self.data[i] if self._is_index(i) else None return self.data[i] if self._is_index(i) else None

View File

@ -1,10 +1,11 @@
import io from io import BytesIO
from typing import BinaryIO from typing import BinaryIO
from alpha_vantage.techindicators import TechIndicators from datetime import datetime
from alpha_vantage.timeseries import TimeSeries
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pandas as pd import pandas as pd
from datetime import datetime from alpha_vantage.techindicators import TechIndicators
from alpha_vantage.timeseries import TimeSeries
class Market: class Market:
@ -13,28 +14,33 @@ class Market:
self.ti = TechIndicators(key=key, output_format='pandas') self.ti = TechIndicators(key=key, output_format='pandas')
self.ts = TimeSeries(key=key, output_format='pandas') self.ts = TimeSeries(key=key, output_format='pandas')
@staticmethod
def sort_data_and_plot(data: pd.DataFrame, label: str, since: datetime, x: str, y: str, ax):
data = data.reset_index()
data['date'] = data['date'].astype('datetime64[ns]')
data = data.loc[since < data['date']]
data = data.loc[data[y] > 0.0].dropna() # Remove zero values
data.plot(x=x, y=y, ax=ax, label=label)
def get_wma(self, stock: str, since: datetime) -> BinaryIO: def get_wma(self, stock: str, since: datetime) -> BinaryIO:
# Init the plot
plt.clf() plt.clf()
fig = plt.figure() fig = plt.figure()
ax = plt.gca() ax = plt.gca()
ax.set_ylabel('Dollar') ax.set_ylabel('Dollar')
def sort_data_and_plot(data: pd.DataFrame, x: str, y: str, label: str): # Real price
data = data.reset_index()
data['date'] = data['date'].astype('datetime64[ns]')
data = data.loc[since < data['date']]
data = data.loc[data[y] > 0.0].dropna() # Remove zero values
data.plot(x=x, y=y, ax=ax, label=label)
df: pd.DataFrame = self.ts.get_daily(symbol=stock, outputsize='full')[0] 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') Market.sort_data_and_plot(data=df, label='Value', since=since, x='date', y='1. open', ax=ax)
# Add wma for 45, 90, 120 days
for period in [45, 90, 120]: for period in [45, 90, 120]:
df: pd.DataFrame = self.ti.get_wma(symbol=stock, interval='daily', time_period=period)[0] 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)) Market.sort_data_and_plot(data=df, label=str(period), since=since, x='date', y='WMA', ax=ax)
# Draw and return png as buffer
plt.plot() plt.plot()
buffer = io.BytesIO() buffer = BytesIO()
fig.savefig(buffer, format='png') fig.savefig(buffer, format='png')
buffer.seek(0) buffer.seek(0)
return buffer return buffer

View File

@ -1,24 +1,32 @@
import matplotlib as mpl
from telegram.ext import CommandHandler from telegram.ext import CommandHandler
from Utils import updater from utils import updater, persistence
from Commands import watchlist_add, watchlist_delete, watchlist_all, watchlist_clear, \ from commands.config import config_handler
set_api_key, get_api_key, start, data, send_updates from commands.watchlist import watchlist_add, watchlist_delete, watchlist_all, watchlist_clear
from commands.other import start, data, send_updates
def main(): def main():
# Setup
mpl.use('agg')
dp = updater.dispatcher dp = updater.dispatcher
jq = updater.job_queue
# Handlers
dp.add_handler(CommandHandler('add', watchlist_add)) dp.add_handler(CommandHandler('add', watchlist_add))
dp.add_handler(CommandHandler('delete', watchlist_delete)) dp.add_handler(CommandHandler('delete', watchlist_delete))
dp.add_handler(CommandHandler('list', watchlist_all)) dp.add_handler(CommandHandler('list', watchlist_all))
dp.add_handler(CommandHandler('clear', watchlist_clear)) dp.add_handler(CommandHandler('clear', watchlist_clear))
dp.add_handler(CommandHandler('setkey', set_api_key)) dp.add_handler(config_handler)
dp.add_handler(CommandHandler('getkey', get_api_key))
dp.add_handler(CommandHandler('start', start)) dp.add_handler(CommandHandler('start', start))
dp.add_handler(CommandHandler('data', data)) dp.add_handler(CommandHandler('data', data))
# Cron jobs
jq.run_repeating(send_updates, interval=30, first=0)
# Start
print('Started 🚀') print('Started 🚀')
send_updates()
updater.start_polling() updater.start_polling()
updater.idle() updater.idle()

15
src/text.py Normal file
View File

@ -0,0 +1,15 @@
INTRO_TEXT = """
*Welcome! 👋*
*1.* First you will need to get a (free) api token for the stock data.
[https://www.alphavantage.co/support/#api-key](Alphavantage Key 🔑)
*2.* Then enter it by sending the code to me with `/setKey myApiCode`
*3.* Add stocks or ETFs to your `/list` by going to [https://finance.yahoo.com/](Yahoo Finance 📈) and the sending it to `/add`
_Example_ For Apple `/add AAPL`
*4.* Optionally choose how often you will get updates by calling the `/frequency` command.
Enjoy 🚀
""".lstrip()

View File

@ -1,24 +1,16 @@
from datetime import datetime, timedelta
from enum import Enum
from yaml import load, Loader
from telegram import Update from telegram import Update
from telegram.ext import PicklePersistence, Updater from telegram.ext import PicklePersistence, Updater
from yaml import load, Loader
from enum import Enum
import pickle
DB_FILE = './data.db' DB_FILE = './data.db'
CONFIG_FILE = './config.yml' CONFIG_FILE = './config.yml'
DEFAULT_DATA = {
'user_data': {},
'chat_data': {},
'conversations': {},
}
try:
pickle.load(open(DB_FILE, 'rb'))
except:
pickle.dump(DEFAULT_DATA, open(DB_FILE, 'wb'))
config = load(open(CONFIG_FILE, 'r'), Loader=Loader) config = load(open(CONFIG_FILE, 'r'), Loader=Loader)
persistence = PicklePersistence(DB_FILE) persistence = PicklePersistence(DB_FILE)
# persistence.load_singlefile()
updater: Updater = Updater(config['token'], use_context=True, persistence=persistence) updater: Updater = Updater(config['token'], use_context=True, persistence=persistence)
@ -26,8 +18,25 @@ class Section(Enum):
Watchlist = 'watchlist' Watchlist = 'watchlist'
API_Key = 'api_key' API_Key = 'api_key'
Running = 'running' Running = 'running'
Interval = 'interval' # Time axis of the graph
Frequency = 'frequency' # How ofter updates should be sent
LastRun = 'last_run'
def current_timestamp():
return int(datetime.now().timestamp())
def delta_timestamp(**kwargs):
return int(timedelta(**kwargs).total_seconds())
def parse_command(update: Update) -> (str, str): def parse_command(update: Update) -> (str, str):
key, value = (update.message.text.split(' ', 1)[1].split(' ', 1) + [None])[:2] key, value = (update.message.text.split(' ', 1)[1].split(' ', 1) + [None])[:2]
return key, value return key, value
def parse_callback(update: Update) -> str:
selected = update.callback_query.data
cleaned = ''.join(selected.split(':')[1:]) # Remove the pattern from the start
return cleaned