new menu for more granular control over the single stocks

This commit is contained in:
cupcakearmy 2019-12-19 16:28:37 +01:00
parent 6f0930a0cc
commit e1c93963d9
9 changed files with 341 additions and 193 deletions

View File

@ -1,37 +0,0 @@
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

View File

@ -1,23 +1,35 @@
from enum import Enum
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, ENABLED = range(3)
MENU, API_KEY, FREQUENCY, ENABLED = range(4)
class Section(Enum):
Watchlist = 'watchlist' # The list of Stocks/ETF to watch
Code = 'code' # Market code for a given stock, etf, etc.
API_Key = 'api_key' # Alpha Vantage API Key
Running = 'running' # Currently sending updates. Avoid overloading the API
Enabled = 'enabled' # Whether the bot should send automatic updates
Interval = 'interval' # Time axis of the graph
Frequency = 'frequency' # How ofter updates should be sent
LastRun = 'last_run' # Last time an update was sent to the user
CurrentToEdit = 'current_to_edit' # Current element to edit in the conversation handler
def show_menu(update: Update, context: CallbackContext):
keyboard = [
[InlineKeyboardButton('API Key', callback_data=API_KEY)],
[InlineKeyboardButton('Auto Updates', callback_data=ENABLED)],
[InlineKeyboardButton('Frequency', callback_data=FREQUENCY)],
[InlineKeyboardButton(
f'Turn {"off" if context.user_data.setdefault(Section.Enabled.value, True) else "on"} global auto updates',
callback_data=ENABLED)],
[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'Auto Updates: *{context.user_data[Section.Enabled.value]}*\n'
f'Frequency: *{context.user_data[Section.Frequency.value]}*\n'
f'API Key: *{context.user_data.get(Section.API_Key.value, "No Api key set")}*\n'
f'Global auto updates: *{context.user_data[Section.Enabled.value]}*\n'
'\nWhat settings do you want to configure?',
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(keyboard, one_time_keyboard=True)
@ -35,27 +47,7 @@ def show_menu_api_key(update: Update, context: CallbackContext):
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):
def init(update: Update, context: CallbackContext):
context.bot.delete_message(
chat_id=update.message.chat_id,
message_id=update.message.message_id,
@ -73,8 +65,6 @@ def menu(update: Update, context: CallbackContext):
if selected == API_KEY:
return show_menu_api_key(update, context)
elif selected == FREQUENCY:
return show_menu_frequency(update, context)
elif selected == ENABLED:
toggle_enabled(update, context)
else:
@ -89,25 +79,8 @@ def set_api_key(update, context):
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 toggle_enabled(update: Update, context: CallbackContext):
new = not context.user_data.setdefault(Section.Enabled.value, True)
context.user_data[Section.Enabled.value] = new
update.effective_user.send_message('Auto updates enabled' if new else 'Auto updates disabled')
context.user_data[Section.Enabled.value] = not context.user_data[Section.Enabled.value]
return show_menu(update, context)
@ -118,16 +91,13 @@ def cancel(update: Update, context: CallbackContext):
config_handler = ConversationHandler(
entry_points=[CommandHandler('config', config)],
entry_points=[CommandHandler('settings', init)],
states={
MENU: [CallbackQueryHandler(menu)],
API_KEY: [
CommandHandler('cancel', cancel),
MessageHandler(Filters.all, set_api_key),
],
FREQUENCY: [CallbackQueryHandler(set_frequency)],
},
fallbacks=[CommandHandler('cancel', cancel)]
)

View File

@ -1,74 +1,97 @@
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 import Update, ParseMode, ReplyKeyboardRemove
from telegram.ext import CallbackContext, ConversationHandler
from telegram.ext.dispatcher import run_async
from commands.config import Section
from market import Market
from text import INTRO_TEXT
from utils import Section, persistence, updater, current_timestamp, delta_timestamp
from utils import persistence, updater, current_timestamp, delta_timestamp
SENDING = False
def error(update: Update, context: CallbackContext):
print(context.error)
def error_handler(update: Update, context: CallbackContext):
print('Error: ', context.error)
def start(update: Update, context: CallbackContext):
def start_handler(update: Update, context: CallbackContext):
update.message.reply_markdown(INTRO_TEXT)
def help_handler(update: Update, context: CallbackContext):
update.message.reply_markdown(INTRO_TEXT)
def stop_handler(update: Update, context: CallbackContext):
context.user_data.clear()
update.message.reply_text('You and your data were deleted 🗑')
@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)
send_update_to_user(update.effective_user['id'], False)
def send_update_to_user(user: str, delta: int):
def send_update_to_user(user: str, auto, codes=None):
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 ⛔️')
updater.bot.send_message(user, text='API Key not set ⛔️\nSet in /settings')
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 🌎')
now = current_timestamp()
if auto:
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))
for code in codes if codes else user_data.get(Section.Watchlist.value, {}).keys():
try:
code_data = persistence.user_data[user][Section.Watchlist.value][code]
code_data = code_data['value']
print(code, code_data)
last_run = code_data[Section.LastRun.value]
frequency = parse(code_data[Section.Frequency.value])
interval = parse(code_data[Section.Interval.value])
print(code, last_run + frequency, now, last_run + frequency - now)
if auto and last_run + frequency > now:
continue
persistence.user_data[user][Section.Watchlist.value][code][Section.LastRun.value] = current_timestamp()
persistence.flush()
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 {code}... ⏳')
delta = datetime.fromtimestamp(now - interval)
chart = market.get_wma(code, delta)
msg.delete()
updater.bot.send_photo(user, photo=chart, disable_notification=True,
caption=f'{code} - {code_data[Section.Interval.value]}')
except Exception as e:
print(f'{user} - {e}')
updater.bot.send_message(user, text=f'There was an error ⚠️\n {e}')
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}')
if auto:
updater.bot.send_message(user, text=f'Done ✅')
finally:
if user_data:
user_data[Section.Running.value] = False
@ -81,16 +104,9 @@ def send_updates(context: CallbackContext):
return
SENDING = True
now = current_timestamp()
for user, user_data in persistence.user_data.items():
enabled = user_data.setdefault(Section.Enabled.value, True)
last_run = user_data.setdefault(Section.LastRun.value, 0)
frequency = parse(user_data.setdefault(Section.Frequency.value, '1d'))
if enabled and last_run + frequency < now:
delta = now - user_data.setdefault(Section.Interval.value, delta_timestamp(days=365))
send_update_to_user(user=user, delta=delta)
if user_data.setdefault(Section.Enabled.value, False):
send_update_to_user(user=user, auto=True)
except Exception as e:
print(e)
finally:

View File

@ -1,45 +1,232 @@
from telegram import Update
from telegram.ext import CallbackContext
from telegram import Update, InlineKeyboardButton, ParseMode, InlineKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, Filters
from limited_list import LimitedList
from utils import parse_command, config, Section
from commands.config import Section
from commands.other import send_update_to_user
from limited_dict import LimitedDict
from utils import parse_command, config
ALL, SINGLE, EDIT, ADD, DELETE, BACK, ENABLED, FREQUENCY, INTERVAL, DATA = map(chr, range(10))
END = str(ConversationHandler.END)
def get_watchlist(context: CallbackContext) -> LimitedList:
return LimitedList(
def get_watchlist(context: CallbackContext) -> LimitedDict:
return LimitedDict(
config[Section.Watchlist.value]['max_items'],
context.user_data.setdefault(Section.Watchlist.value, []),
context.user_data.setdefault(Section.Watchlist.value, {}),
)
def save_watchlist(context: CallbackContext, l: LimitedList):
context.user_data[Section.Watchlist.value] = l.all()
def save_watchlist(context: CallbackContext, limited_dict: LimitedDict):
context.user_data[Section.Watchlist.value] = limited_dict.dict
def watchlist_add(update: Update, context: CallbackContext):
value, *rest = parse_command(update)
def init(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 show_menu(update: Update, context: CallbackContext):
wl = get_watchlist(context)
wl.add(str(value).upper())
save_watchlist(context, wl)
update.message.reply_text('Saved 💾')
saved = [
[InlineKeyboardButton(item, callback_data=item)]
for item in wl.all()
]
options = [[
InlineKeyboardButton('Add', callback_data=ADD),
InlineKeyboardButton('Done', callback_data=END)
]]
update.effective_user.send_message(
'_Your Watchlist:_\n'
f'*{len(wl)}/{wl.limit}* slots filled\n'
'\nYou can add, modify or delete items',
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(saved + options, one_time_keyboard=True)
)
return ALL
def watchlist_delete(update: Update, context: CallbackContext):
value, *rest = parse_command(update)
def menu(update: Update, context: CallbackContext):
context.bot.delete_message(
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
)
selected: str = update.callback_query.data
if selected == ADD:
return show_add(update, context)
elif selected == END:
return ConversationHandler.END
else:
context.user_data[Section.CurrentToEdit.value] = selected
return show_single(update, context)
def show_add(update: Update, context: CallbackContext):
update.effective_user.send_message(
'Send me the code (e.g. AAPL or URTH)\n'
'or send /cancel',
reply_markup=ReplyKeyboardRemove()
)
return ADD
def add(update, context):
reply: str = update.message.text
wl = get_watchlist(context)
found = wl.delete(value)
wl[reply.upper()] = {
Section.Enabled.value: True,
Section.Frequency.value: '1d',
Section.Interval.value: '52w',
Section.LastRun.value: 0,
}
save_watchlist(context, wl)
update.message.reply_text('Deleted 🗑' if found else 'Not found ❓')
update.message.reply_text(f'Saved {reply} 💾', reply_markup=ReplyKeyboardRemove())
return show_menu(update, context)
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 show_single(update, context):
current = context.user_data[Section.CurrentToEdit.value]
current_data = get_watchlist(context)[current]
keyboard = [
[InlineKeyboardButton(f'Turn {"off" if current_data[Section.Enabled.value] else "on"} auto updates', callback_data=ENABLED)],
[InlineKeyboardButton(f'Frequency', callback_data=FREQUENCY)],
[InlineKeyboardButton(f'Time interval', callback_data=INTERVAL)],
[InlineKeyboardButton(f'Show data', callback_data=DATA)],
[InlineKeyboardButton('Delete', callback_data=DELETE)],
[InlineKeyboardButton('Back', callback_data=BACK)],
]
update.effective_user.send_message(
'_Current settings:_\n'
f'Auto Updates: *{current_data[Section.Enabled.value]}*\n'
f'Frequency: *{current_data[Section.Frequency.value]}*\n'
f'Interval: *{current_data[Section.Interval.value]}*\n'
f'\nEdit {current}: ⬇',
parse_mode=ParseMode.MARKDOWN,
reply_markup=InlineKeyboardMarkup(keyboard)
)
return SINGLE
def watchlist_clear(update: Update, context: CallbackContext):
def single(update, context):
current = context.user_data[Section.CurrentToEdit.value]
selected = update.callback_query.data
wl = get_watchlist(context)
wl.clear()
save_watchlist(context, wl)
update.message.reply_text('Cleared 🧼')
context.bot.delete_message(
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
)
if selected == DELETE:
update.effective_user.send_message(f'Deleted {current} 💪')
del wl[current]
save_watchlist(context, wl)
return show_menu(update, context)
elif selected == BACK:
return show_menu(update, context)
elif selected == ENABLED:
print('Changing', wl[Section.Enabled.value])
wl[current][Section.Enabled.value] = not wl[current][Section.Enabled.value]
save_watchlist(context, wl)
return show_single(update, context)
elif selected == FREQUENCY:
return show_single_frequency(update, context)
elif selected == INTERVAL:
return show_single_interval(update, context)
elif selected == DATA:
send_update_to_user(update.effective_user['id'], False, [current])
return show_single(update, context)
else:
return cancel(update, context)
def show_single_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 single_frequency(update: Update, context: CallbackContext):
return save_single_attribute(update, context, Section.Frequency.value)
def show_single_interval(update: Update, context: CallbackContext):
keyboard = [
[InlineKeyboardButton('1 day', callback_data='1d'), InlineKeyboardButton(
'2 days', callback_data='2d')],
[InlineKeyboardButton('1 week', callback_data='7d'), InlineKeyboardButton(
'1 weeks', callback_data='14d')],
[InlineKeyboardButton('1 month', callback_data='30d'), InlineKeyboardButton(
'3 months', callback_data='90d')],
[InlineKeyboardButton('1 year', callback_data='52w'), InlineKeyboardButton(
'2 years', callback_data='104w')],
[InlineKeyboardButton('Cancel', callback_data='cancel')],
]
update.effective_user.send_message(
'Select the graph time span 📈:',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return INTERVAL
def single_interval(update: Update, context: CallbackContext):
return save_single_attribute(update, context, Section.Interval.value)
def save_single_attribute(update: Update, context: CallbackContext, key):
current = context.user_data[Section.CurrentToEdit.value]
selected = update.callback_query.data
wl = get_watchlist(context)
context.bot.delete_message(
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
)
if selected != 'cancel':
update.effective_user.send_message(f'Saved {selected} 💾')
wl[current][key] = selected
return show_single(update, context)
def cancel(update: Update, context: CallbackContext):
update.effective_user.send_message('Canceled', reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
cancel_command_handler = CommandHandler('cancel', cancel)
watchlist_handler = ConversationHandler(
entry_points=[CommandHandler('list', init)],
states={
ALL: [CallbackQueryHandler(menu)],
ADD: [cancel_command_handler, MessageHandler(Filters.text, add)],
SINGLE: [cancel_command_handler, CallbackQueryHandler(single)],
FREQUENCY: [CallbackQueryHandler(single_frequency)],
INTERVAL: [CallbackQueryHandler(single_interval)],
},
fallbacks=[cancel_command_handler],
)

16
src/generator_dict.py Normal file
View File

@ -0,0 +1,16 @@
class GeneratorDict(dict):
def __init__(self, *args, generator=None, **kwargs):
super().__init__(*args, **kwargs)
if not generator:
def generator(x): return x
self.generator = generator
def __getitem__(self, key):
try:
return super().__getitem__(key)
except:
value = self.generator(key)
super().__setitem__(key, value)
return value

View File

@ -1,38 +1,44 @@
import time
import math
from math import inf
class LimitedDict:
def __init__(self, init: dict, limit: int):
def __init__(self, limit: int, init: dict):
self.dict = init
self.limit = limit
def set(self, key, value):
# Delete oldest element if there are too many
def __len__(self):
return len(self.dict)
def __setitem__(self, key, value):
# Delete the oldest if there are too many entries
if len(self.dict) + 1 > self.limit:
timestamp = math.inf
who = None
timestamp = inf
oldest = None
for cur, item in self.dict.items():
if item['when'] < timestamp:
timestamp = item['when']
who = cur
del self.dict[who]
oldest = cur
del self.dict[oldest]
self.dict[key] = {
'value': value,
'when': int(time.time())
}
def get(self, key):
def __getitem__(self, key):
value = self.dict.get(key, None)
return value['value'] if value else None
def delete(self, key):
def __delitem__(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()]
return {
key: value['value']
for key, value in self.dict.items()
}

View File

@ -11,10 +11,13 @@ class LimitedList:
self.data = init if init else []
self.limit = limit
def __len__(self):
return len(self.data)
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):
def add(self, value: any):
print(f'Before {self.data}')
# Delete oldest element if there are too many
if len(self.data) + 1 > self.limit:

View File

@ -1,10 +1,10 @@
import matplotlib as mpl
from telegram.ext import CommandHandler
from utils import updater, persistence
from utils import updater
from commands.config import config_handler
from commands.watchlist import watchlist_add, watchlist_delete, watchlist_all, watchlist_clear
from commands.other import start, data, send_updates
from commands.watchlist import watchlist_handler
from commands.other import data, send_updates, start_handler, help_handler, stop_handler, error_handler
def main():
@ -14,16 +14,16 @@ def main():
jq = updater.job_queue
# Handlers
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(config_handler)
dp.add_handler(CommandHandler('start', start))
dp.add_error_handler(error_handler)
dp.add_handler(CommandHandler('start', start_handler))
dp.add_handler(CommandHandler('stop', stop_handler))
dp.add_handler(CommandHandler('help', help_handler))
dp.add_handler(CommandHandler('data', data))
dp.add_handler(config_handler)
dp.add_handler(watchlist_handler)
# Cron jobs
jq.run_repeating(send_updates, interval=30, first=0)
jq.run_repeating(send_updates, interval=30, first=5)
# Start
print('Started 🚀')

View File

@ -10,19 +10,9 @@ CONFIG_FILE = './config.yml'
config = load(open(CONFIG_FILE, 'r'), Loader=Loader)
persistence = PicklePersistence(DB_FILE)
# persistence.load_singlefile()
updater: Updater = Updater(config['token'], use_context=True, persistence=persistence)
class Section(Enum):
Watchlist = 'watchlist'
API_Key = 'api_key'
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())
@ -32,11 +22,8 @@ def delta_timestamp(**kwargs):
def parse_command(update: Update) -> (str, str):
"""
Splits the command from the rest of the message and returns the tuple
"""
key, value = (update.message.text.split(' ', 1)[1].split(' ', 1) + [None])[:2]
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