From b8b3b49896b7553c8ad57e0f3fbb4464ecf24179 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Fri, 4 Mar 2022 13:31:21 -0500 Subject: [PATCH] Groupme Bot (#1418) * Added groupme helpers * Began handling of user input * Expanded commands, improved bot * Completed dictionary * Eliminated redundancies * Added more * Added debuggers for server * Fixed bugs * Improved input handling and image sending * Fixed dd-est * Fixed dd commands * Added dps fix * Fixed feargreed * Fixed econ commands * Fixed gov commands * Fixed ta * Added docstrings * Added docstrings * black Co-authored-by: didierlopes.eth --- bots/cmds/dps_cmds.py | 12 +- bots/cmds/economy_cmds.py | 33 +- bots/cmds/options_cmds.py | 9 +- bots/cmds/screener_cmds.py | 114 +-- bots/cmds/ta_cmds.py | 20 - bots/common/commands_dict.py | 809 ++++++++++++++++++ bots/economy/indices.py | 1 + bots/economy/overview.py | 1 + bots/groupme/groupme_helpers.py | 75 ++ bots/groupme/run_groupme.py | 139 +++ bots/helpers.py | 26 +- bots/run_discordbot.py | 13 +- bots/stocks/government/contracts.py | 15 +- bots/stocks/screener/financial.py | 2 +- gamestonk_terminal/forex/av_view.py | 3 +- .../stocks/.stocks_controller.py.swp | Bin 32768 -> 0 bytes 16 files changed, 1087 insertions(+), 185 deletions(-) create mode 100644 bots/common/commands_dict.py create mode 100644 bots/groupme/groupme_helpers.py create mode 100644 bots/groupme/run_groupme.py delete mode 100644 gamestonk_terminal/stocks/.stocks_controller.py.swp diff --git a/bots/cmds/dps_cmds.py b/bots/cmds/dps_cmds.py index 04f9988fa45..d11453a2f1f 100644 --- a/bots/cmds/dps_cmds.py +++ b/bots/cmds/dps_cmds.py @@ -12,15 +12,7 @@ from bots.stocks.dark_pool_shorts.psi import psi_command from bots.stocks.dark_pool_shorts.shorted import shorted_command from bots.stocks.dark_pool_shorts.sidtc import sidtc_command from bots.stocks.dark_pool_shorts.spos import spos_command - -pos_choices = { - "Short Vol (1M)": "sv", - "Short Vol %": "sv_pct", - "Net Short Vol (1M)": "nsv", - "Net Short Vol ($100M)": "nsv_dollar", - "DP Position (1M)": "dpp", - "DP Position ($1B)": "dpp_dollar", -} +from bots.common import commands_dict class DarkPoolShortsCommands(commands.Cog): @@ -57,7 +49,7 @@ class DarkPoolShortsCommands(commands.Cog): async def pos( self, inter: disnake.AppCmdInter, - sort: str = commands.Param(choices=pos_choices), + sort: str = commands.Param(choices=commands_dict.dps_pos_choices), num: int = 10, ): """Dark pool short position [Stockgrid] diff --git a/bots/cmds/economy_cmds.py b/bots/cmds/economy_cmds.py index e0fbb70a322..c4a602ebecc 100644 --- a/bots/cmds/economy_cmds.py +++ b/bots/cmds/economy_cmds.py @@ -16,34 +16,7 @@ from bots.economy.softs import softs_command from bots.economy.usbonds import usbonds_command from bots.economy.valuation import valuation_command from bots.helpers import ShowView - -group = [ - "basic_materials", - "capitalization", - "communication_services", - "consumer_cyclical", - "consumer_defensive", - "country", - "energy", - "financial", - "healthcare", - "industry", - "industrials", - "real_estate", - "sector", - "technology", - "utilities", -] - -fgind = { - "Junk Bond Demand": "jbd", - "Market Volatility": "mv", - "Put and Call Options": "pco", - "Market Momentum": "mm", - "Stock Price Strength": "sps", - "Stock Price Breadth": "spb", - "Safe Heaven Demand": "shd", -} +from bots.common import commands_dict class EconomyCommands(commands.Cog): @@ -116,7 +89,7 @@ class EconomyCommands(commands.Cog): async def valuation( self, inter: disnake.AppCmdInter, - economy_group: str = commands.Param(choices=group), + economy_group: str = commands.Param(choices=commands_dict.econ_group), ): """Valuation of sectors, industry, country [Finviz] @@ -132,7 +105,7 @@ class EconomyCommands(commands.Cog): async def performance( self, inter: disnake.AppCmdInter, - economy_group: str = commands.Param(choices=group), + economy_group: str = commands.Param(choices=commands_dict.econ_group), ): """Performance of sectors, industry, country [Finviz] diff --git a/bots/cmds/options_cmds.py b/bots/cmds/options_cmds.py index 95a36794881..4dd3f860c3e 100644 --- a/bots/cmds/options_cmds.py +++ b/bots/cmds/options_cmds.py @@ -17,12 +17,7 @@ from bots.stocks.options.unu import unu_command from bots.stocks.options.vol import vol_command from bots.stocks.options.vsurf import vsurf_command from bots.stocks.quote import quote_command - -vsurf_choices = { - "Volatility": "IV", - "Open Interest": "OI", - "Last Price": "LP", -} +from bots.common import commands_dict class SlashCommands(commands.Cog): @@ -268,7 +263,7 @@ class SlashCommands(commands.Cog): self, inter: disnake.AppCmdInter, ticker: str = commands.Param(autocomplete=ticker_autocomp), - z: str = commands.Param(choices=vsurf_choices), + z: str = commands.Param(choices=commands_dict.options_vsurf_choices), ): """Display Volatility Surface diff --git a/bots/cmds/screener_cmds.py b/bots/cmds/screener_cmds.py index 21f2ff0edad..2b249431edb 100644 --- a/bots/cmds/screener_cmds.py +++ b/bots/cmds/screener_cmds.py @@ -13,108 +13,8 @@ from bots.stocks.screener.presets_custom import presets_custom_command from bots.stocks.screener.presets_default import presets_default_command from bots.stocks.screener.technical import technical_command from bots.stocks.screener.valuation import valuation_command +from bots.common import commands_dict -sort = { - "overview": [ - "Ticker", - "Company", - "Sector", - "Industry", - "Country", - "Market Cap", - "P/E", - "Price", - "Change", - "Volume", - ], - "valuation": [ - "Ticker", - "Market Cap", - "P/E", - "Fwd P/E", - "PEG", - "P/S", - "P/B", - "P/C", - "P/FCF", - "EPS this Y", - "EPS next Y", - "EPS past 5Y", - "EPS next 5Y", - "Sales past 5Y", - "Price", - "Change", - "Volume", - ], - "financial": [ - "Ticker", - "Market Cap", - "Dividend", - "ROA", - "ROE", - "ROI", - "Curr R", - "Quick R", - "LTDebt/Eq", - "Debt/Eq", - "Gross M", - "Oper M", - "Profit M", - "Earnings", - "Price", - "Change", - "Volume", - ], - "ownership": [ - "Ticker", - "Market Cap", - "Outstanding", - "Float", - "Insider Own", - "Insider Trans", - "Inst Own", - "Inst Trans", - "Float Short", - "Short Ratio", - "Avg Volume", - "Price", - "Change", - "Volume", - ], - "performance": [ - "Ticker", - "Perf Week", - "Perf Month", - "Perf Quart", - "Perf Half", - "Perf Year", - "Perf YTD", - "Volatility W", - "Volatility M", - "Recom", - "Avg Volume", - "Rel Volume", - "Price", - "Change", - "Volume", - ], - "technical": [ - "Ticker", - "Beta", - "ATR", - "SMA20", - "SMA50", - "SMA200", - "52W High", - "52W Low", - "RSI", - "Price", - "Change", - "from Open", - "Gap", - "Volume", - ], -} # pylint: disable=R0912 @@ -157,7 +57,7 @@ class ScreenerCommands(commands.Cog): self, inter: disnake.AppCmdInter, preset: str = commands.Param(autocomplete=presets_custom_autocomp), - sort: str = commands.Param(choices=sort["overview"]), + sort: str = commands.Param(choices=commands_dict.screener_sort["overview"]), limit: int = 5, ascend: bool = False, ): @@ -179,7 +79,7 @@ class ScreenerCommands(commands.Cog): self, inter: disnake.AppCmdInter, preset: str = commands.Param(autocomplete=presets_custom_autocomp), - sort: str = commands.Param(choices=sort["valuation"]), + sort: str = commands.Param(choices=commands_dict.screener_sort["valuation"]), limit: int = 5, ascend: bool = False, ): @@ -201,7 +101,7 @@ class ScreenerCommands(commands.Cog): self, inter: disnake.AppCmdInter, preset: str = commands.Param(autocomplete=presets_custom_autocomp), - sort: str = commands.Param(choices=sort["financial"]), + sort: str = commands.Param(choices=commands_dict.screener_sort["financial"]), limit: int = 5, ascend: bool = False, ): @@ -223,7 +123,7 @@ class ScreenerCommands(commands.Cog): self, inter: disnake.AppCmdInter, preset: str = commands.Param(autocomplete=presets_custom_autocomp), - sort: str = commands.Param(choices=sort["ownership"]), + sort: str = commands.Param(choices=commands_dict.screener_sort["ownership"]), limit: int = 5, ascend: bool = False, ): @@ -245,7 +145,7 @@ class ScreenerCommands(commands.Cog): self, inter: disnake.AppCmdInter, preset: str = commands.Param(autocomplete=presets_custom_autocomp), - sort: str = commands.Param(choices=sort["performance"]), + sort: str = commands.Param(choices=commands_dict.screener_sort["performance"]), limit: int = 5, ascend: bool = False, ): @@ -267,7 +167,7 @@ class ScreenerCommands(commands.Cog): self, inter: disnake.AppCmdInter, preset: str = commands.Param(autocomplete=presets_custom_autocomp), - sort: str = commands.Param(choices=sort["technical"]), + sort: str = commands.Param(choices=commands_dict.screener_sort["technical"]), limit: int = 5, ascend: bool = False, ): diff --git a/bots/cmds/ta_cmds.py b/bots/cmds/ta_cmds.py index ed96782d9b7..ecfc8d753ca 100644 --- a/bots/cmds/ta_cmds.py +++ b/bots/cmds/ta_cmds.py @@ -29,26 +29,6 @@ from bots.stocks.technical_analysis.view import view_command from bots.stocks.technical_analysis.wma import wma_command from bots.stocks.technical_analysis.zlma import zlma_command -possible_ma = [ - "dema", - "ema", - "fwma", - "hma", - "linreg", - "midpoint", - "pwma", - "rma", - "sinwma", - "sma", - "swma", - "t3", - "tema", - "trima", - "vidya", - "wma", - "zlma", -] - class TechnicalAnalysisCommands(commands.Cog): """Technical Analysis menu.""" diff --git a/bots/common/commands_dict.py b/bots/common/commands_dict.py new file mode 100644 index 00000000000..eca02974354 --- /dev/null +++ b/bots/common/commands_dict.py @@ -0,0 +1,809 @@ +import re +from typing import List + +import pandas as pd +from bots.economy.currencies import currencies_command +from bots.economy.energy import energy_command +from bots.economy.feargreed import feargreed_command +from bots.economy.futures import futures_command +from bots.economy.glbonds import glbonds_command +from bots.economy.grains import grains_command +from bots.economy.indices import indices_command +from bots.economy.meats import meats_command +from bots.economy.metals import metals_command +from bots.economy.overview import overview_command +from bots.economy.performance import performance_command +from bots.economy.softs import softs_command +from bots.economy.usbonds import usbonds_command +from bots.economy.valuation import valuation_command +from bots.stocks.candle import candle_command +from bots.stocks.dark_pool_shorts.dpotc import dpotc_command +from bots.stocks.dark_pool_shorts.ftd import ftd_command +from bots.stocks.dark_pool_shorts.hsi import hsi_command +from bots.stocks.dark_pool_shorts.pos import pos_command +from bots.stocks.dark_pool_shorts.psi import psi_command +from bots.stocks.dark_pool_shorts.shorted import shorted_command +from bots.stocks.dark_pool_shorts.sidtc import sidtc_command +from bots.stocks.dark_pool_shorts.spos import spos_command +from bots.stocks.disc.ford import ford_command +from bots.stocks.due_diligence.analyst import analyst_command +from bots.stocks.due_diligence.arktrades import arktrades_command +from bots.stocks.due_diligence.customer import customer_command +from bots.stocks.due_diligence.est import est_command +from bots.stocks.due_diligence.pt import pt_command +from bots.stocks.due_diligence.sec import sec_command +from bots.stocks.due_diligence.supplier import supplier_command +from bots.stocks.government.contracts import contracts_command +from bots.stocks.government.gtrades import gtrades_command +from bots.stocks.government.histcont import histcont_command +from bots.stocks.government.lastcontracts import lastcontracts_command +from bots.stocks.government.lasttrades import lasttrades_command +from bots.stocks.government.lobbying import lobbying_command +from bots.stocks.government.qtrcontracts import qtrcontracts_command +from bots.stocks.government.topbuys import topbuys_command +from bots.stocks.government.toplobbying import toplobbying_command +from bots.stocks.government.topsells import topsells_command +from bots.stocks.insider.lins import lins_command +from bots.stocks.options.cc_hist import cc_hist_command +from bots.stocks.options.hist import hist_command +from bots.stocks.options.iv import iv_command +from bots.stocks.options.oi import oi_command +from bots.stocks.options.opt_chain import chain_command +from bots.stocks.options.overview import overview_command as overview_opt_command +from bots.stocks.options.unu import unu_command +from bots.stocks.options.vol import vol_command +from bots.stocks.options.vsurf import vsurf_command +from bots.stocks.quote import quote_command +from bots.stocks.screener.financial import financial_command +from bots.stocks.screener.historical import historical_command +from bots.stocks.screener.overview import overview_command as overview_screener_command +from bots.stocks.screener.ownership import ownership_command +from bots.stocks.screener.performance import ( + performance_command as performance_screener_command, +) +from bots.stocks.screener.presets_custom import presets_custom_command +from bots.stocks.screener.presets_default import presets_default_command +from bots.stocks.screener.technical import technical_command +from bots.stocks.screener.valuation import ( + valuation_command as valuation_screener_command, +) +from bots.stocks.technical_analysis.ad import ad_command +from bots.stocks.technical_analysis.adosc import adosc_command +from bots.stocks.technical_analysis.adx import adx_command +from bots.stocks.technical_analysis.aroon import aroon_command +from bots.stocks.technical_analysis.bbands import bbands_command +from bots.stocks.technical_analysis.cci import cci_command +from bots.stocks.technical_analysis.donchian import donchian_command +from bots.stocks.technical_analysis.ema import ema_command +from bots.stocks.technical_analysis.fib import fib_command +from bots.stocks.technical_analysis.fisher import fisher_command +from bots.stocks.technical_analysis.hma import hma_command +from bots.stocks.technical_analysis.kc import kc_command +from bots.stocks.technical_analysis.macd import macd_command +from bots.stocks.technical_analysis.obv import obv_command +from bots.stocks.technical_analysis.recom import recom_command +from bots.stocks.technical_analysis.rsi import rsi_command +from bots.stocks.technical_analysis.sma import sma_command +from bots.stocks.technical_analysis.stoch import stoch_command +from bots.stocks.technical_analysis.summary import summary_command +from bots.stocks.technical_analysis.view import view_command +from bots.stocks.technical_analysis.wma import wma_command +from bots.stocks.technical_analysis.zlma import zlma_command + +re_date = re.compile(r"/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/") +re_int = re.compile(r"^[1-9]\d*$") +re_float = re.compile(r"^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$") +re_name = re.compile(r"(?s).*") +re_window = re.compile(r"(?s).*") + +gov_type = ["congress", "senate", "house"] + +dps_pos_choices = { + "Short Vol (1M)": "sv", + "Short Vol %": "sv_pct", + "Net Short Vol (1M)": "nsv", + "Net Short Vol ($100M)": "nsv_dollar", + "DP Position (1M)": "dpp", + "DP Position ($1B)": "dpp_dollar", +} + +econ_group = [ + "basic_materials", + "capitalization", + "communication_services", + "consumer_cyclical", + "consumer_defensive", + "country", + "energy", + "financial", + "healthcare", + "industry", + "industrials", + "real_estate", + "sector", + "technology", + "utilities", +] + +econ_fgind = { + "Junk Bond Demand": "jbd", + "Market Volatility": "mv", + "Put and Call Options": "pco", + "Market Momentum": "mm", + "Stock Price Strength": "sps", + "Stock Price Breadth": "spb", + "Safe Heaven Demand": "shd", +} + +options_vsurf_choices = { + "Volatility": "IV", + "Open Interest": "OI", + "Last Price": "LP", +} + +opt_intervals = [1, 5, 15, 30, 60, 1440] + + +screener_sort = { + "overview": [ + "Ticker", + "Company", + "Sector", + "Industry", + "Country", + "Market Cap", + "P/E", + "Price", + "Change", + "Volume", + ], + "valuation": [ + "Ticker", + "Market Cap", + "P/E", + "Fwd P/E", + "PEG", + "P/S", + "P/B", + "P/C", + "P/FCF", + "EPS this Y", + "EPS next Y", + "EPS past 5Y", + "EPS next 5Y", + "Sales past 5Y", + "Price", + "Change", + "Volume", + ], + "financial": [ + "Ticker", + "Market Cap", + "Dividend", + "ROA", + "ROE", + "ROI", + "Curr R", + "Quick R", + "LTDebt/Eq", + "Debt/Eq", + "Gross M", + "Oper M", + "Profit M", + "Earnings", + "Price", + "Change", + "Volume", + ], + "ownership": [ + "Ticker", + "Market Cap", + "Outstanding", + "Float", + "Insider Own", + "Insider Trans", + "Inst Own", + "Inst Trans", + "Float Short", + "Short Ratio", + "Avg Volume", + "Price", + "Change", + "Volume", + ], + "performance": [ + "Ticker", + "Perf Week", + "Perf Month", + "Perf Quart", + "Perf Half", + "Perf Year", + "Perf YTD", + "Volatility W", + "Volatility M", + "Recom", + "Avg Volume", + "Rel Volume", + "Price", + "Change", + "Volume", + ], + "technical": [ + "Ticker", + "Beta", + "ATR", + "SMA20", + "SMA50", + "SMA200", + "52W High", + "52W Low", + "RSI", + "Price", + "Change", + "from Open", + "Gap", + "Volume", + ], +} + +presets = [ + "potential_reversals", + "golden_cross_penny", + "rosenwald_gtfo", + "golden_cross", + "bull_runs_over_10pct", + "recent_growth_and_support", + "heavy_inst_ins", + "short_squeeze_scan", + "under_15dol_stocks", + "top_performers_healthcare", + "oversold_under_3dol", + "value_stocks", + "cheap_dividend", + "death_cross", + "top_performers_tech", + "unusual_volume", + "cheap_oversold", + "undervalue", + "high_vol_and_low_debt", + "simplistic_momentum_scanner_under_7dol", + "5pct_above_low", + "growth_stocks", + "cheap_bottom_dividend", + "analyst_strong_buy", + "oversold", + "rosenwald", + "weak_support_and_top_performers", + "channel_up_and_low_debt_and_sma_50and200", + "template", + "modified_neff", + "buffett_like", + "oversold_under_5dol", + "sexy_year", + "news_scanner", + "top_performers_all", + "stocks_strong_support_levels", + "continued_momentum_scan", + "modified_dreman", + "break_out_stocks", +] +signals = [ + "top_gainers", + "top_losers", + "new_high", + "new_low", + "most_volatile", + "most_active", + "unusual_volume", + "overbought", + "oversold", + "downgrades", + "upgrades", + "earnings_before", + "earnings_after", + "recent_insider_buying", + "recent_insider_selling", + "major_news", + "horizontal_sr", + "tl_resistance", + "tl_support", + "wedge_up", + "wedge_down", + "wedge", + "triangle_ascending", + "triangle_descending", + "channel_up", + "channel_down", + "channel", + "double_top", + "double_bottom", + "multiple_top", + "multiple_bottom", + "head_shoulders", + "head_shoulders_inverse", +] + +possible_ma = [ + "dema", + "ema", + "fwma", + "hma", + "linreg", + "midpoint", + "pwma", + "rma", + "sinwma", + "sma", + "swma", + "t3", + "tema", + "trima", + "vidya", + "wma", + "zlma", +] + + +def get_tickers() -> List[str]: + col_list = ["Name"] + df = pd.read_csv("files/tickers.csv", usecols=col_list) + df = df["Name"] + return df.tolist() + + +tickers = get_tickers() + +commands = { + "dd-analyst": { + "function": analyst_command, + "required": {"ticker": tickers}, + }, + "dd-pt": { + "function": pt_command, + "required": {"ticker": tickers}, + "optional": {"raw": [True, False], "start": re_date}, + }, + "dd-est": {"function": est_command, "required": {"ticker": tickers}}, + "dd-sec": {"function": sec_command, "required": {"ticker": tickers}}, + "dd-supplier": { + "function": supplier_command, + "required": {"ticker": tickers}, + }, + "dd-customer": { + "function": customer_command, + "required": {"ticker": tickers}, + }, + "dd-arktrades": { + "function": arktrades_command, + "required": { + "ticker": tickers, + }, + "optional": {"num": re_int}, + }, + "dps-shorted": {"function": shorted_command, "optional": re_int}, + "dps-hsi": {"function": hsi_command, "optional": {"num": re_int}}, + "dps-pos": { + "function": pos_command, + "required": {"sort": dps_pos_choices.values()}, + "optional": {"num": re_int}, + }, + "dps-sidtc": { + "function": sidtc_command, + "required": {"sort": ["float", "dtc", "si"]}, + "optional": {"num": re_int}, + }, + "dps-ftd": { + "function": ftd_command, + "required": {"ticker": tickers}, + "optional": {"start": re_date, "end": re_date}, + }, + "dps-dpotc": { + "function": dpotc_command, + "required": {"ticker": tickers}, + }, + "dps-spos": {"function": spos_command, "required": {"ticker": tickers}}, + "dps-psi": {"function": psi_command, "required": {"ticker": tickers}}, + "econ-feargreed": { + "function": feargreed_command, + }, + "econ-overview": { + "function": overview_command, + }, + "econ-indices": { + "function": indices_command, + }, + "econ-futures": { + "function": futures_command, + }, + "econ-usbonds": { + "function": usbonds_command, + }, + "econ-glbonds": { + "function": glbonds_command, + }, + "econ-energy": { + "function": energy_command, + }, + "econ-metals": { + "function": metals_command, + }, + "econ-meats": { + "function": meats_command, + }, + "econ-grains": { + "function": grains_command, + }, + "econ-softs": { + "function": softs_command, + }, + "econ-currencies": { + "function": currencies_command, + }, + "econ-valuation": { + "function": valuation_command, + "required": {"economy_group": econ_group}, + }, + "econ-performance": { + "function": performance_command, + "required": {"economy_group": econ_group}, + }, + "gov-lasttrades": { + "function": lasttrades_command, + "required": {"gov_type": gov_type}, + "optional": {"past_days": re_int, "representative": re_name}, + }, + "gov-topbuys": { + "function": topbuys_command, + "required": {"gov_type": gov_type}, + "optional": { + "past_transactions_months": re_int, + "num": re_int, + "raw": [True, False], + }, + }, + "gov-topsells": { + "function": topsells_command, + "required": {"gov_type": gov_type}, + "optional": { + "past_transactions_months": re_int, + "num": re_int, + "raw": [True, False], + }, + }, + "gov-lastcontracts": { + "function": lastcontracts_command, + "optional": {"past_transactions_days": re_int, "num": re_int}, + }, + "gov-qtrcontracts": { + "function": qtrcontracts_command, + "required": {"analysis": ["total", "upmom", "downmom"]}, + "optional": { + "num": re_int, + }, + }, + "gov-toplobbying": { + "function": toplobbying_command, + "optional": {"num": re_int, "raw": [True, False]}, + }, + "gov-gtrades": { + "function": gtrades_command, + "required": { + "ticker": tickers, + "gov_type": gov_type, + }, + "optional": {"past_transactions_months": re_int, "raw": [True, False]}, + }, + "gov-contracts": { + "function": contracts_command, + "required": { + "ticker": tickers, + "past_transaction_days": re_int, + "raw": [True, False], + }, + }, + "gov-histcont": {"function": histcont_command, "required": {"ticker": tickers}}, + "gov-lobbying": { + "function": lobbying_command, + "required": {"ticker": tickers}, + "optional": { + "num": re_int, + }, + }, + "opt-chain": { + "function": chain_command, + "required": { + "ticker": tickers, + "expiry": re_date, + "opt_type": ["Calls", "Puts"], + }, + "optional": {"min_sp": re_float, "max_sp": re_float}, + }, + "opt-oi": { + "function": oi_command, + "required": {"ticker": tickers, "expiry": re_date}, + "optional": {"min_sp": re_float, "max_sp": re_float}, + }, + "opt-iv": { + "function": iv_command, + "required": {"ticker": tickers}, + }, + "q": {"function": quote_command, "required": {"ticker": tickers}}, + "disc-ford": { + "function": ford_command, + }, + "opt-unu": { + "function": unu_command, + }, + "ins-last": { + "function": lins_command, + "required": {"ticker": tickers}, + "optional": {"num": re_int}, + }, + "cc": { + "function": candle_command, + "required": {"ticker": tickers, "interval": opt_intervals}, + "optional": {"past_days": re_int, "start": re_date, "end": re_date}, + }, + "opt-overview": { + "function": overview_opt_command, + "required": {"ticker": tickers, "expiry": re_date}, + "optional": { + "min_sp": re_float, + "max_sp": re_float, + }, + }, + "opt-vol": { + "function": vol_command, + "required": {"ticker": tickers, "expiry": re_date}, + }, + "opt-vsurf": { + "function": vsurf_command, + "required": {"ticker": tickers, "z": options_vsurf_choices.values()}, + }, + "opt-hist": { + "function": hist_command, + "required": { + "ticker": tickers, + "expiry": re_date, + "strike": re_float, + "opt_type": ["Calls", "Puts"], + "greek": ["iv", "gamma", "delta", "theta", "rho", "vega"], + }, + }, + "opt-cc-hist": { + "function": cc_hist_command, + "required": { + "ticker": tickers, + "expiry": re_date, + "strike": re_float, + "opt_type": ["Calls", "Puts"], + }, + }, + "scr-presets_default": { + "function": presets_default_command, + }, + "scr-presets_custom": { + "function": presets_custom_command, + }, + "scr-historical": { + "function": historical_command, + "required": {"signal": signals}, + "optional": {"start": re_date}, + }, + "scr-overview": { + "function": overview_screener_command, + "required": {"preset": presets, "sort": screener_sort["overview"]}, + "optional": {"limit": re_int, "ascend": [True, False]}, + }, + "scr-valuation": { + "function": valuation_screener_command, + "required": {"preset": presets, "sort": screener_sort["valuation"]}, + "optional": {"limit": re_int, "ascend": [True, False]}, + }, + "scr-financial": { + "function": financial_command, + "required": {"preset": presets, "sort": screener_sort["financial"]}, + "optional": {"limit": re_int, "ascend": [True, False]}, + }, + "scr-ownership": { + "function": ownership_command, + "required": {"preset": presets, "sort": screener_sort["ownership"]}, + "optional": {"limit": re_int, "ascend": [True, False]}, + }, + "scr-performance": { + "function": performance_screener_command, + "required": {"preset": presets, "sort": screener_sort["performance"]}, + "optional": {"limit": re_int, "ascend": [True, False]}, + }, + "scr-technical": { + "function": technical_command, + "required": {"preset": presets, "sort": screener_sort["technical"]}, + "optional": {"limit": re_int, "ascend": [True, False]}, + }, + "ta-ema": { + "function": ema_command, + "required": {"ticker": tickers}, + "optional": { + "window": re_window, + "offset": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-sma": { + "function": sma_command, + "required": {"ticker": tickers}, + "optional": { + "window": re_window, + "offset": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-wma": { + "function": wma_command, + "required": {"ticker": tickers}, + "optional": { + "window": re_window, + "offset": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-hma": { + "function": hma_command, + "required": {"ticker": tickers}, + "optional": { + "window": re_window, + "offset": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-zlma": { + "function": zlma_command, + "required": {"ticker": tickers}, + "optional": { + "window": re_window, + "offset": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-cci": { + "function": cci_command, + "required": {"ticker": tickers}, + "optional": { + "length": re_int, + "scalar": re_float, + "start": re_date, + "end": re_date, + }, + }, + "ta-macd": { + "function": macd_command, + "required": {"ticker": tickers}, + "optional": { + "length": re_int, + "scalar": re_float, + "start": re_date, + "end": re_date, + }, + }, + "ta-rsi": { + "function": rsi_command, + "required": {"ticker": tickers}, + "optional": { + "length": re_int, + "scalar": re_float, + "drift": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-stoch": { + "function": stoch_command, + "required": {"ticker": tickers}, + "optional": { + "fast_k": re_int, + "slow_k": re_int, + "slow_d": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-fisher": { + "function": fisher_command, + "required": {"ticker": tickers}, + "optional": {"length": re_int, "start": re_date, "end": re_date}, + }, + "ta-cg": { + "function": fisher_command, + "required": {"ticker": tickers}, + "optional": {"length": re_int, "start": re_date, "end": re_date}, + }, + "ta-adx": { + "function": adx_command, + "required": {"ticker": tickers}, + "optional": { + "length": re_int, + "scalar": re_int, + "drift": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-aroon": { + "function": aroon_command, + "required": {"ticker": tickers}, + "optional": { + "length": re_int, + "scalar": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-bbands": { + "function": bbands_command, + "required": { + "ticker": tickers, + "mamode": ["ema", "sma", "wma", "hma", "zlma"], + }, + "optional": {"length": re_int, "std": re_int, "start": re_date, "end": re_date}, + }, + "ta-donchian": { + "function": donchian_command, + "required": {"ticker": tickers}, + "optional": { + "upper_length": re_int, + "lower_length": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-kc": { + "function": kc_command, + "required": { + "ticker": tickers, + "mamode": ["ema", "sma", "wma", "hma", "zlma"], + }, + "optional": { + "length": re_int, + "scalar": re_int, + "offset": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-ad": { + "function": ad_command, + "required": {"ticker": tickers}, + "optional": {"is_open": ["True", "False"], "start": re_date, "end": re_date}, + }, + "ta-adosc": { + "function": adosc_command, + "required": { + "ticker": tickers, + }, + "optional": { + "is_open": ["True", "False"], + "fast": re_int, + "slow": re_int, + "start": re_date, + "end": re_date, + }, + }, + "ta-obv": { + "function": obv_command, + "required": {"ticker": tickers}, + "optional": {"start": re_date, "end": re_date}, + }, + "ta-fib": { + "function": fib_command, + "required": {"ticker": tickers}, + "optional": {"start": re_date, "end": re_date}, + }, + "ta-view": {"function": view_command, "required": {"ticker": tickers}}, + "ta-summary": {"function": summary_command, "required": {"ticker": tickers}}, + "ta-recom": {"function": recom_command, "required": {"ticker": tickers}}, +} diff --git a/bots/economy/indices.py b/bots/economy/indices.py index 56e92bc978a..f9c5a71d1dd 100644 --- a/bots/economy/indices.py +++ b/bots/economy/indices.py @@ -35,6 +35,7 @@ def indices_command(): df = df[ [ + " ", "Price", "Chg", "%Chg", diff --git a/bots/economy/overview.py b/bots/economy/overview.py index 61aa8b08d64..fbbef56869a 100644 --- a/bots/economy/overview.py +++ b/bots/economy/overview.py @@ -30,6 +30,7 @@ def overview_command(): df = df[ [ + " ", "Price", "Chg", "%Chg", diff --git a/bots/groupme/groupme_helpers.py b/bots/groupme/groupme_helpers.py new file mode 100644 index 00000000000..68399ca87dc --- /dev/null +++ b/bots/groupme/groupme_helpers.py @@ -0,0 +1,75 @@ +"""Groupme""" +__docformat__ = "numpy" + +from typing import Optional +import io +import json +import os +import pathlib +import urllib.request as urllib + +import requests +from dotenv import load_dotenv + +load_dotenv() + +base = "https://api.groupme.com/v3" +TOKEN = os.getenv("GROUPME_TOKEN") +end = f"?token={TOKEN}" + +group_to_bot = { + os.getenv("TEST_GROUP_ID"): os.getenv("TEST_GROUP_BOT"), + os.getenv("MAIN_GROUP_ID"): os.getenv("MAIN_GROUP_BOT"), +} + + +def shorten_message(message: Optional[str]) -> Optional[str]: + # TODO: make the cutoff algorithm better + if message is None: + return message + if len(message) > 990: + return f"{message[:990]}..." + return message + + +def upload_image(image: str, local: bool) -> requests.Response: + url = "https://image.groupme.com/pictures" + headers = { + "Content-Type": "image/jpeg", + "X-Access-Token": os.getenv("X_ACCESS_TOKEN"), + } + if local: + path_string = pathlib.Path(__file__).parent.parent.resolve() + path = os.path.join(path_string, image) + return requests.post(url, data=open(path, "rb").read(), headers=headers) + with urllib.urlopen(image) as data: + return requests.post(url, data=io.BytesIO(data.read()), headers=headers) + + +def send_message(message: str, group_id: str) -> requests.Response: + mid = "/bots/post" + bot_id = group_to_bot[group_id] + cleaned = shorten_message(message) + return requests.post(url=f"{base+mid}?bot_id={bot_id}&text={cleaned}") + + +def send_image( + image: str, group_id: str, text: str = None, local: bool = False +) -> requests.Response: + if "http" in image: + response = upload_image(image, False) + else: + response = upload_image(image, True) + response_json = response.json() + image_url = response_json["payload"]["picture_url"] + mid = "/bots/post" + bot_id = group_to_bot[group_id] + text = shorten_message(text) + data = { + "bot_id": bot_id, + "text": text, + "attachments": [{"type": "image", "url": image_url}], + } + if "http" not in image: + os.remove(image) + return requests.post(base + mid + end, data=json.dumps(data)) diff --git a/bots/groupme/run_groupme.py b/bots/groupme/run_groupme.py new file mode 100644 index 00000000000..e56b22a1172 --- /dev/null +++ b/bots/groupme/run_groupme.py @@ -0,0 +1,139 @@ +import json +from typing import List, Pattern, Dict, Any, Set, Union + +from bots.common.commands_dict import commands +from bots.groupme.groupme_helpers import send_message +from bots.helpers import ShowView + + +def get_syntax(selected: Dict[str, Any], cmd: str) -> str: + """Returns the syntax for a given command + + Parameters + ---------- + selected : Dict[str, Any] + The command object + cmd : str + The command that was attempted + + Returns + --------- + syntax : str + The syntax for the given command + """ + + syntax = f"{cmd}/" + syntax += "/".join(selected.get("required", [])) + return syntax + + +def get_arguments(selected: Dict[str, Any], req_name: str, group_id: str) -> None: + """Returns the arguments for a given command + + Parameters + ---------- + selected : Dict[str, Any] + The command object + req_name : str + The name of the requirement + group_id : str + The groupme chat id + """ + + if req_name == "ticker": + send_message("Please give a listed ticker", group_id) + elif req_name == "past_transactions_days": + send_message("Please give the number of days as an integer", group_id) + elif req_name == "raw": + send_message("Please type true or false", group_id) + else: + select = [str(x) for x in selected["required"].get(req_name, [])] + selections = ", ".join(select) + if len(selections) < 990: + selections = f"{selections[:990]}" + send_message(f"Options: {selections}", group_id) + + +def send_options(name: str, items: Union[List[Any], Set[Any]], group_id: str) -> None: + """Sends the options for a user + + Parameters + ---------- + name : str + The name of the section + items : List[str] + The items the user can select from + group_id : str + The groupme chat id + """ + message = name + clean = list(items) + clean.sort() + message += ", ".join(list(clean)) + send_message(message, group_id) + + +def handle_groupme(request) -> bool: + """Handles groupme bot inputs + + Parameters + ---------- + request : Request + The request object provided by FASTAPI + + Returns + ---------- + success : bool + Whether the response was sent successfully + """ + + req = json.loads(request.decode("utf-8")) + text = req.get("text").strip().lower() + group_id = req.get("group_id").strip() + + if text[0] == "!": + cmd = text[1:] + full_cmd = cmd.split("/") + group = full_cmd[0].split("-")[0] + parents = {x.split("-")[0] for x in commands} + if group in parents: + if full_cmd[0] in commands: + selected = commands[full_cmd[0]] + if len(full_cmd) != len(selected.get("required", [])) + 1: + syntax = get_syntax(selected, full_cmd[0]) + send_message(f"Required syntax: {syntax}", group_id) + return False + other_args = {} + for i, val in enumerate(full_cmd[1:]): + req_name = list(selected.get("required", {}).keys())[i] + required = selected.get("required", [])[req_name] + if isinstance(required, List) and required != [True, False]: + required = [str(x) for x in required] + if isinstance(val, str) and req_name in ["ticker"]: + val = val.upper() + elif isinstance(val, str) and req_name == "raw": + val = bool(val) + if (isinstance(required, List) and val not in required) or ( + isinstance(required, Pattern) and not required.match(val) + ): + syntax = get_syntax(selected, full_cmd[0]) + send_message( + f"{syntax}\nInvalid argument for: {req_name}", group_id + ) + get_arguments(selected, req_name, group_id) + return False + other_args[req_name] = val + func = selected["function"] + ShowView().groupme(func, group_id, cmd, **other_args) + return True + else: + show_cmds = [] + for command in commands: + if group == command[: len(group)]: + show_cmds.append(command) + send_options("Valid commands: ", show_cmds, group_id) + return False + else: + send_options("Valid categories: ", parents, group_id) + return False + return False diff --git a/bots/helpers.py b/bots/helpers.py index 32cfd9a5ed2..d5ea5e300bc 100644 --- a/bots/helpers.py +++ b/bots/helpers.py @@ -1,4 +1,5 @@ import os +from typing import List import uuid import df2img @@ -9,6 +10,7 @@ from numpy.core.fromnumeric import transpose from PIL import Image import bots.config_discordbot as cfg +from bots.groupme.groupme_helpers import send_image, send_message presets_custom = [ "potential_reversals", @@ -201,7 +203,9 @@ def image_border(file, **kwargs): if "fig" in kwargs: fig = kwargs["fig"] fig.write_image(imagefile) - img = Image.open(file) + img = Image.open(imagefile) + else: + img = Image.open(file) im_bg = Image.open(cfg.IMG_BG) h = img.height + 240 w = img.width + 520 @@ -218,7 +222,6 @@ def image_border(file, **kwargs): image = Image.open(file) image = autocrop_image(image, 0) image.save(imagefile, "PNG", quality=100) - os.remove(file) return imagefile @@ -269,3 +272,22 @@ class ShowView: ) await inter.send(embed=embed, delete_after=30.0) + + def groupme(self, func, group_id, name, *args, **kwargs): + data = func(*args, **kwargs) + if "imagefile" in data: + send_image(data["imagefile"], group_id, data.get("description", ""), True) + elif "embeds_img" in data: + send_image( + data["embeds_img"][0], group_id, data.get("description", ""), True + ) + elif "description" in data: + title = data.get("title", "") + # TODO: Allow navigation through pages + description = data.get("description") + if isinstance(description, List): + clean_desc = description[0].replace("Page ", "") + else: + clean_desc = description.replace("Page ", "") + message = f"{title}\n{clean_desc}" + send_message(message, group_id) diff --git a/bots/run_discordbot.py b/bots/run_discordbot.py index 8bb974a48f4..93c79d9d230 100644 --- a/bots/run_discordbot.py +++ b/bots/run_discordbot.py @@ -8,17 +8,26 @@ import config_discordbot as cfg import disnake from config_discordbot import logger from disnake.ext import commands -from fastapi import FastAPI +from fastapi import FastAPI, Request + +from bots.groupme.run_groupme import handle_groupme app = FastAPI() -# For next update @app.get("/") async def read_root(): return {"Hello": str(gst_bot.user)} +@app.post("/") +async def write_root(request: Request): + # TODO: Make this work for more bots + req_info = await request.body() + value = handle_groupme(req_info) + return {"Worked": value} + + activity = disnake.Activity( type=disnake.ActivityType.watching, name="Gamestonk Terminal: https://github.com/GamestonkTerminal/GamestonkTerminal", diff --git a/bots/stocks/government/contracts.py b/bots/stocks/government/contracts.py index 2aea0894a9f..dba827081c2 100644 --- a/bots/stocks/government/contracts.py +++ b/bots/stocks/government/contracts.py @@ -1,8 +1,9 @@ -import pandas as pd -from matplotlib import pyplot as plt +from typing import Union -import bots.config_discordbot as cfg +from matplotlib import pyplot as plt +import pandas as pd from bots.config_discordbot import logger +import bots.config_discordbot as cfg from bots.helpers import image_border from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal.helper_funcs import plot_autoscale @@ -10,9 +11,10 @@ from gamestonk_terminal.stocks.government import quiverquant_model def contracts_command( - ticker: str = "", past_transaction_days: int = 10, raw: bool = False + ticker: str = "", past_transaction_days: Union[int, str] = 10, raw: bool = False ): """Displays contracts associated with tickers [quiverquant.com]""" + past_transaction_days = int(past_transaction_days) # Debug user input if cfg.DEBUG: logger.debug("gov-contracts %s %s %s", ticker, past_transaction_days, raw) @@ -24,7 +26,10 @@ def contracts_command( df_contracts = quiverquant_model.get_government_trading("contracts", ticker) if df_contracts.empty: - raise Exception("No government contracts found") + return { + "title": f"Stocks: [quiverquant.com] Contracts by {ticker}", + "description": f"{ticker} does not have any contracts", + } # Output Data df_contracts["Date"] = pd.to_datetime(df_contracts["Date"]).dt.date diff --git a/bots/stocks/screener/financial.py b/bots/stocks/screener/financial.py index d079bc9bf2a..240c7ea2989 100644 --- a/bots/stocks/screener/financial.py +++ b/bots/stocks/screener/financial.py @@ -115,7 +115,7 @@ def financial_command(preset="template", sort="", limit: int = 5, ascend: bool = ) ) reports.append(f"{description}") - + print(df_screen) return { "view": Menu, "title": title, diff --git a/gamestonk_terminal/forex/av_view.py b/gamestonk_terminal/forex/av_view.py index 04b65b7c4b5..e5be9700e18 100644 --- a/gamestonk_terminal/forex/av_view.py +++ b/gamestonk_terminal/forex/av_view.py @@ -87,7 +87,7 @@ def display_candle( candle_chart_kwargs["figratio"] = (10, 7) candle_chart_kwargs["figscale"] = 1.10 candle_chart_kwargs["figsize"] = plot_autoscale() - fig, _ = mpf.plot(data, **candle_chart_kwargs) + fig, ax = mpf.plot(data, **candle_chart_kwargs) fig.suptitle( f"{from_symbol}/{to_symbol}", x=0.055, @@ -95,6 +95,7 @@ def display_candle( horizontalalignment="left", ) theme.visualize_output(force_tight_layout=False) + ax[0].legend() else: if len(external_axes) != 1: logger.error("Expected list of 1 axis items.") diff --git a/gamestonk_terminal/stocks/.stocks_controller.py.swp b/gamestonk_terminal/stocks/.stocks_controller.py.swp deleted file mode 100644 index c9901cc5afebd34b4faa1a86a5a5eb3c23dd33b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI43zQ^Pd4LOoAR42@Bod-nG%n19nchW3J*$HZGqdc)Y$iaApgG2vlMp!(^NHTPNLcG4k76FL0V}82PQm z88>KpEvK<{-dw3f<`My_i{V!NQ>pr`DDWE%Hz>E)C|SM)gY$s|9-|WIMD@`ZJjr^_ zv(Mfp5;(ndn*G#Yyy!71j6t;r5*SEeAc27d1`-%ZU?72k1O^g#Y)T+%A8%bj)sKy< zzYzZ(llwg`ci+q1e_VXtjDP<&cmMeK{80RRZk&P1=Y;sWAOF5LcmL<&>!b1Ssd0f# z{P^Ms18#?pz`NiESb|r;LAU@;f~UY&e!;SCf*Op#e?Q5xz6!U)hv7PSEnE#Rhm+yE zKW|y@hxfsia0R>^E`^uDS#Tme9v*z6Wql3qf?MF-@W=2PxB@(wg+1_M*b2{tr@%4r z{U=z~eQ+<_3Ll1d!JFZC;c|!|goAK0JPrQwM9aDp-UELIN8owz;Nvap0k{`F4pn#& zoCcrAm~ji-49oB$I2P{12y!D_2rq#$oD5&YcycZ5fgSJ=#+Dmk6uye#<2CRs_yTRz zk#>TgK1zmegkE?Uv`u+wFNbvO}-wHJo6? zUhr_p2t1rUM(nB|1UQRCp5Gd=>-cfBs-Dvbk1RN0CGx6^Zcqs$Cx})p$#gE2>72XZ zEO~w>a2nZM$+RB$&FyyJ)@oifVk`HLXxWP>mHglyGGfpB{(Qq#sm}XLrY2loa)VaW zZACW!bw=#VqHZM?{7&fFQQ*|P*1QxQcM_*6sZxu!A2^X~H~hJ|6>h79ad7D-%->$k z4U*{QPB(d^fJcknc3Z6#U1oc(vm#~o+qPe?dsU*hJ3$rMxS`4*tOh0-o4;MRMG)=n zk}0`VYRPk#x3=A&?gvez?2g!-kb>Inzzu1gy4OH1I@ww~DR<2Y7VWn0i;!z}xZnqo zU2`Hw(vKTgf=CD?t!{fQQlcs}XjIeX^pb`P-72lGQo~WG6Gp*Gs{Oi&sy@~7+z^3K zClzVLKGdO4*ht#*)%8>gHCh3eqhLklSo6ZF5smyU5L&MSbTngK6)uTJbolFliOX3{r`1E0YsQ4=P4nvgj&}PWGYbJ3dbQ|@uQ>#_H zR^4BnRmHUqh!fU!dv+`JcA5u|sQ!tM)?o&66a|L#*n)n`cElU88VOD4KJ z@uU+AXjW&W(hgmR0i#oo|L&K%5>A|wiYu`e+(uiK)gH6!rBW%blTMJ^$uvdRB21}d zdv$sea?oP0<=>ZM(AE#qRS!L{TxR!bvxY0~i;E2ZICC4F-ge!TY`|vk?MB)$g1khRrelTsmr3Ffva=*aY(mPj{-)Lo`v;%&Eg^AMjU_(73@-*SzhcI?_sDaL1Za@`Nh zZ6{hNzsmPoL*}IB1uZ-^hbk2wIc}vgOv~+`8lT)VRc_Wwx#~q*Qg%XSG`TY%WTl)iWV^RHbjS0!W`Sk*7~XFB6xCZu%|{&dezfp7c47+Dxeq zWzw3@x7C}AgUyE6*a?$HvK2=uXeTJgrRgI^@ptkB=?8Zh4Wd*B-f+^ zSq*=FUJWbxgC}*Fh2w5@js7CthW>&dqTOD>dpMHe6FPGZcWnRJ&p&(Hh}UEl7LEFy zNY+}TteYC{lH0H>HF5R&2Q%4-f3U6N>6t22W8V$>#^EWrAg;u9Z{9@YEc97S_REIn z5f9Lck_8KzIJ*KF^i4sQ3ML+lKNT84_#_STGI!q#0U>I813-f-qa+X-!%c;o)nX6A24m57z_ zq8F9phlI?~af9a`6;^zCMXNooB)4J`i&omQP>qAgWUFf<4J}VxbH|*cC2$qL`^Yir z`l0zN9!%zxZlQXKWmPIQzp4}ho(Rk>rBwYCr8;%5?nB`d1>Xyercx{06SCsAr}mDYFPifH zsmWavGqx-!ZI+6e1tza4=2DF{jk2;*B*v6p(~cA&8@O~4cBnIzt?fnHHgxWpV(q{r z82OkPDWj~dQ5Q}VV+s|@RgGKuoipM$KOKF--^Vyc1yb3`1<>I>R|vmd?5a)UM@6r}b~pY$c@>>7%$^(;;G& zDSavZcWJuiO2NdCs%;h+F56CZ(V0gR3|+Swih$-^yXm3UwdS|mxh7AimTgfhearmq z9UYiO6vHXU1&H!j4$?w{NPDcCP6;7$u8wh7? zbQ=h&%z_&Tr+fC=0mTfHei=9(rD$q1ZgrvMQO$6oYs=`LRw5HrCv_sDORM=qo9%JA z!u8L+pRqyPG?*dO)I+5(Uz=JPtudK7OuNZ1dnbwE)!Z<4TGLh;duleg398#5xyN;7 zpUh5q$$eDHeSwbwi@hF7ZKfg7G^G?`m1VE6QIVU!{A zto*YB=aEc>#dB0H{mo5*y-7bRQ=iBweH0J$q%#+Hx%9vm{r@CZ33thUa%%tI==~?4 z^WThKKMTjfhtTE!1RAgi)9^I-Df;}s!aZ<3tinrS0?vkQa1wk2{r+R{Mu=b={4(5w zet#Rtp8T8ON~pn0pbW>rr_l311lPgUAo~Bsa2{-dzd`@M72W{zumzqBpJea-6Yy4O z!Y&wrd)a&cJlp~AhQn|GhT%zY2Yc~1LkJ%1hV4*-6X3h-&wl~l2v>s#2Ve|NgA?I< z?A!l6dE8#p!3W_wSb|Gn3{C{u^ZzzTTiq$`l|Ry5qcl1i zILoOAkg^j6YD*8RQHRHOl*}F3jwI&4vC;wAAEvLcm8wQ^qtk3Lg2Srbc}*oP`7*Sf zP9qwd2|BswC{d!ZQs0PdyEC(mm6+_r8i&V#n@gIi^o?B_O&hw8x{ebIzRy~Uy;WaT zkLPzN)t<|*)^sK0UaoLh_uMfyPqIVJNTZYseJ~qL1XtSa&+m8h$*MbC?gW`l3%M*u z>0&8wUQp`JZa>}XDzrq{$wqjtFe>;&0|6A>m}bYj==yl=RYh^)ZLKlAr_s%JbTlWk zqi&)dvYeK7S@4(j4!rc3CD(2@{D_6A+72)0N^0ca+Z)&KZn~*;>bYd;iG8lOQz4I{ ziqfwGPx&G+;HY8Pb*a${;CP!~HaAi^hX(paX|&zk^(4Aawkv{UPD!l=jOT(@N_tiU z`gXh?@L696YQH13GkFwigzQw7q$W-w$yWYZcBx#l&(hbz4lX%|yZhMjsXW8eak}Es z%d&WLiR}vqbgecil@gze=!Zb81Lum9A8ht`sz>Y?&QdH;9I|p`?x;N-3#ipM+Ao z;-%4&Xn(Syp4^$;c94Hb`Lg-wr`VKuFe#ml4UswiM1z~c_b3v7q!qtoFMHvFnqORY zVp=L>BpZ?3Xx04X-kC+7W?EjkVW%DiEUuHmBGolh5u2KpoSTEWq%f7zsFO-JZZ{c? z8+&Tcpq<9vF;u)M`g%>O$Ig^mj*IsZ8%*Mk7v}30xCn-o`B-Ump+MZ(@|y=A-+Efc z3(qq3l3zWAs?L_Gi`>afzqxxzq$rAXjU@S_5J=DDD(VRJmzHPBt1gIbLA3c;UMlmv zO!PFH*+7?i$ni5b&BpcUH{gMa`o1k}_6hy_u3Wm2h$x zty53Hmb#YuuPBwWPE*H7c}SUPvaG}}Ak~57jw!rlf|;E2`YyBfg!n%sj&{<8@R7mY zh>1k|TNcS1XGolf)cqdkqpXH0-g9FzS1I4L5oK3nEuf1%Y^B|2S$MC?b42`Sm3kc) zKQR#4iGe`ecDrtjnARwZ_!=aHsEKW-4O1YX&wMKfOM@egkN9?&dmN?>p zIk%*eoQsl@vhHRReSQUR;_9Nf+=@G}I=VH-RTJ`{h1fS(d7!FP5lpwd(jAl! z{X(_>{|xlV`$eBb|8K@R8g%`uG!-?=E>;azy z@dvmbu7D<74ClZp@D*$Wx4;MBTDS^a5WB&B*aL2ZTj1mHdYFbO*a}aDABt@NZiWi% zgc2MJ;wSJR+y!rkRoDlo!Vj$qjy-=kJ5Q^}e%@2bB&?tNt=RWRlFm)M>pcDoVmv|xe{Xlumv+Y1 z+X`yu#jWL^_(n446jZ#(#VTKU)|p3F>)r^Z(N-#nqge9rbjX%k3b#-EU_(1C0A~)* zx_CLXBC}St(NXU7(gNau6ftKq@7gj`;h(oywr3A0*Tn6($;RI5mZ`b?W)@qqx!2s+ zYp9$bqukuQl4+T=GncZ@*hgX|7F7tvBc(hh3J2mg=5DE7UvszIe3aH*rrx;< z?q%OkE$WpEKAqa$;RU>cBHMMZ)Qc~DfAy6|u^pIIs7%)K+jPd7{!+zRnh$$BoCzq+ zt6tPdX`1Ss9_6kPkM64c-^Y~DyW16~rQE;!XG)DLtnQVa#+ys!tSvYFBi8E7TPzmG z7z43KrKw2z*SyT63#WxzoTy%U8ao00t?h=hQi+WnJUZ4n`U^iD5jTJ4k5$>1mTszM zW}BuF-NxZf8MVr2vK*C-Nf9MGDp{}obZX}BZ11wn<)Ytsm-1q#(Q(!0WHLO8g3C42 zbIMK|BV%odVhvG=;k9dAxTj^Z5IY%3P(zh^124;z$Giz;eoX^0{vr%6*v_|PWuP0)%D;~)hiAb`vsTH zsA8`pvB4FJoDc=#E*3{3+~VBj-I~Oh$QvugqAtEHrOUrTVa}boP+qf0-l&jQDk!<~ z@-7~>CaQWfqp%!Fx6>};y~Rqol4qsZPcKe)=48BQh1qKd9I@AONAH%sTi!LzjL9g= zW^0yjrDty&IcHn0X|zpn9954In3^JG-k`_F_CZSah5&J^6>=of# zQ?{oLJEB@{x5?-^1$h|*SytS7i5!prT=dyk=QdW4F z@ATW0s>=H5)Ne_g+Y6AMoLSNHwD07X{8UO6D_5e@XnM=*D3-XUeUW;%$4Ei61Zc!u zM!M>Ib>nvTk+9u3%d)6byPLTvs^!*N>QkrQj5fV!*Co`uKT;|+{bNXoT)eNaGN~P| zToH{F3R*-9f`oEW3R_C{|Ia{2JRTiV^ndexKH2|&J6r=DkaqxH2&ckv@I`d{FM#+4 zi0=PtScDlk79K{w{}G74zzwhrlkltX0DArXAUglY;LT8hb3k1{r<~g8ph!~ zI1L^L-$uW`4wj(_4LAd~!13^J==FEN3LJo4uoFsfC%XM-;WO~3a1FR{GCYia{{Y+% zH^GhYS`eRs3qf@MTjAp%Hh@2by)X$A@GLkH{t2D`dRPVV9SA^t2S(wS;J?uOzXf-| z`{71-CtL#!cnLg&&i?~=C%gl$hItr=AJSG|hP&WSI0Bc$ESv}OZA59$$HB%989#FL zo9_@t&TFJlyYr z(3^y%D$9TCI9|nXCYIyhl%rX%_f<+naqzxkitj(#qPoCeIyy0}Q~Tum31$8)sC*{F zu1r&cAX;A`aBmX=$Uf?mo|AnPsOtrbM|It(lZ8=uv%cVZk&?Gde*_{W@va1tVik1g ze!X;!^ww#o*yJpmsq$Hhf`&@gbg9N6c;Ck1-Xm_D1L_kDyW;OcjK}ZwrR3FbAU`rW zuA6N)`E|sqKOw;@*80O1-8U1%ds)`9rDAw5D~dx9*8wqYT~IhJu_ zQJ*Z?sALG=xA$>x6OzeJ4U>GxWFK25tg6_T=@CdgqIDP44$0>tVO1|Hy z?pus*vg`Ch`5+6g^!km8-eD^9UiwC5EZ=_V_o(LHCS<&(3?}1mv#j(;d+qL56mP@g zF?ot1pt@nf5VpT$xVZ_@*c{m|C|Baw`Nyye=}#xb7jl~_Dk$Dxd~IS`yd}FDaJg0S z8?vC!K9yAXigcY!P}Pyu3*@{GAAioHWIpFnD##NhCX)c?y&2{Gpn5~FY%HnnxtGmI zzM6uYs5YfqQqc4vE-7R2&Dm6(gXO@*I$CvyO1v~-m(EH#14(=_pqm%F%gUObj5xU; zC&}}6rCy#^FC(RyTvm{A5itR1k@WfRb z^oIShER`z%WxH&miR$kd`7InQ)eNQFWc0Twj4uR!MMHcNj1Bs#-mDhE4d;~37G6u? z810$uHAa1%%{j!PSjSb*MJSD?eK?N7T#%}g9!dB=F#GeC5qsehXnn05ZSn59uab>*llrrf>ct7>A*lFd85tfL${?MpdE_v{&+oE+BfZOZc? zsXUVh8H@feYT`e{`~M6Qb$k#V|Bdila1~qv7JMJw{yXqTa4jssW$;2c9lnTue=ocn zBDe%z2u}wKzJq@MCAb~rTL3E{-~2ldo(ZSI-RSu8oxdBwhcVa!zXqqkkJ0hJ1)qkS z-~jB0ArRgFZukKF0W87GVK%R`4g15mnumX$l40t;H1ik(~_$(ZPHi)i&5!Ahb!ZY9}w4rK8)q-Y(r+?M+yI27-!BmhnUeQwb zixeW00G9b`S6eT?)Jd%dEEC53uDPD*ovyQ!l}RL%VRo94AnPo!(#}rZHGZ#pFDV)8 zh!J3)_oqxSEu>_wPw8HGTlHI*9omdmkQGcK)vPr7HErJ}oy#=k`N>xr3$!^)BqT?Y z`ANxUH6eV6zI)4Q*8OoHlJKc zgqUr@?!5x*M)#iSpM9hDSUI{;`wi?U(-ZW&m3Z>4)epL9_A~8e$M-t@q3b-@(;G5H zvm{qJHZTa;W9>HxqyHbhWR4F}B~$JBu@59Gfz+I*=^6xuol@IesPRI&Eg0 zGG%EJfBh&^VoTDHV*`za*Jr=Rnu|86uKlbHY4sLLpF#V-FsAF7F})*kCH=n})hYkg zo^hQio~8Q9#=1(q|G2JiW?W}y=VW+fTS|YbQjfEPxgH&VI5f4+m+DkA@}H1b6V!*7 z*Ajn{`rz_y!&bLit|8_G=X9{GQOgAt~<5$pcmahFQzVDwM+EUT|v5S66@`X=O lSwiU>-I+P{