diff --git a/simple-stock-bots/simple-discord-stock-bot/credentials.py b/simple-stock-bots/simple-discord-stock-bot/credentials.py new file mode 100644 index 000000000..60f8309c3 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/credentials.py @@ -0,0 +1,4 @@ +secrets = { + 'TOKEN': "https://discordapp.com/developers/applications/ > BOTNAME > Bot > 'copy token'", + 'BRAVOS_API': 'https://bravos.co/a/login' +} diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt new file mode 100644 index 000000000..0fe188316 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -0,0 +1,2 @@ +discord.py==0.16.12 +discord==0.0.2 diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py new file mode 100644 index 000000000..d44d61a49 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -0,0 +1,50 @@ +# Work with Python 3.6 +import json +from discord.ext import commands +import discord +import re +import urllib.request +import credentials + +BOT_PREFIX = ("?", "!") +TOKEN = credentials.secrets['TOKEN'] +BRAVOS_API = credentials.secrets['BRAVOS_API'] + +client = commands.Bot(command_prefix=BOT_PREFIX) + + +@client.event +async def on_ready(): + print('Bot is Ready!!!') + + +@client.event +async def on_message(message): + author = message.author + content = message.content + channel = message.channel + tickers = re.findall('[$](\w{1,4})', content) + print(tickers) + for ticker in tickers: + print(ticker) + url = 'https://data.bravos.co/v1/quote?symbols=' + ticker + \ + '&apikey=' + BRAVOS_API + '&format=json' + print(url) + + with urllib.request.urlopen(url) as url: + data = json.loads(url.read().decode()) + nameTicker = data[ticker.upper()]['name'] + print(nameTicker) + priceTicker = data[ticker.upper()]['price'] + print(str(priceTicker)) + await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $' + str(priceTicker)) + + +async def list_servers(): + await client.wait_until_ready() + while not client.is_closed: + print("Current servers:") + for server in client.servers: + print(server.name) + +client.run(TOKEN) -- 2.51.2 From 77b3f034c2fc4d2542eff1c5974f97f4263cde7f Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Wed, 9 Jan 2019 00:20:15 +0000 Subject: [PATCH 02/75] Add README.md --- simple-stock-bots/simple-discord-stock-bot/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 simple-stock-bots/simple-discord-stock-bot/README.md diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md new file mode 100644 index 000000000..ce13bd170 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -0,0 +1,5 @@ +# Readme + +will fill this out at some point + +*hopefully* \ No newline at end of file -- 2.51.2 From 445ef3bb6c0f387fdbd56876b7fada9b6c9b9444 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 8 Jan 2019 18:07:27 -0700 Subject: [PATCH 03/75] Fixed issue where bot was reading its own messages causing errors. --- simple-stock-bots/simple-discord-stock-bot/stockBot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index d44d61a49..c55e265ae 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -20,6 +20,10 @@ async def on_ready(): @client.event async def on_message(message): + + if message.author == client.user: + return + author = message.author content = message.content channel = message.channel -- 2.51.2 From 9afeaa9e7d16051e2b9d86cff34a2c8223d5cb73 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 8 Jan 2019 18:09:24 -0700 Subject: [PATCH 04/75] Stock now uses only one api call for unlimited number of tickers --- .../simple-discord-stock-bot/stockBot.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index c55e265ae..fc2e33475 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -29,19 +29,19 @@ async def on_message(message): channel = message.channel tickers = re.findall('[$](\w{1,4})', content) print(tickers) - for ticker in tickers: - print(ticker) - url = 'https://data.bravos.co/v1/quote?symbols=' + ticker + \ - '&apikey=' + BRAVOS_API + '&format=json' - print(url) - - with urllib.request.urlopen(url) as url: - data = json.loads(url.read().decode()) - nameTicker = data[ticker.upper()]['name'] - print(nameTicker) - priceTicker = data[ticker.upper()]['price'] - print(str(priceTicker)) - await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $' + str(priceTicker)) + url = 'https://data.bravos.co/v1/quote?symbols=' + ",".join(tickers) + \ + '&apikey=' + BRAVOS_API + '&format=json' + print(url) + with urllib.request.urlopen(url) as url: + data = json.loads(url.read().decode()) + for ticker in tickers: + try: + nameTicker = data[ticker.upper()]['name'] + priceTicker = data[ticker.upper()]['price'] + await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $' + str(priceTicker)) + except KeyError: + await client.send_message(channel, ticker.upper() + ' does not exist.') + pass async def list_servers(): -- 2.51.2 From ae92c88b938bc4522bb47d727fa8af06009502f6 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 8 Jan 2019 18:20:05 -0700 Subject: [PATCH 05/75] added comments --- .../simple-discord-stock-bot/stockBot.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index fc2e33475..311a19afd 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -6,14 +6,15 @@ import re import urllib.request import credentials -BOT_PREFIX = ("?", "!") +# Make sure to update credentials.py with your secrets TOKEN = credentials.secrets['TOKEN'] BRAVOS_API = credentials.secrets['BRAVOS_API'] +BOT_PREFIX = ("?", "!") client = commands.Bot(command_prefix=BOT_PREFIX) -@client.event +@client.event # Make bot say when its ready async def on_ready(): print('Bot is Ready!!!') @@ -21,21 +22,26 @@ async def on_ready(): @client.event async def on_message(message): - if message.author == client.user: + if message.author == client.user: # Prevent bot from reacting to its own messages return + # define information about the message author = message.author content = message.content channel = message.channel + + # regex to find tickers in messages tickers = re.findall('[$](\w{1,4})', content) - print(tickers) + + # get ticker information from bravos api url = 'https://data.bravos.co/v1/quote?symbols=' + ",".join(tickers) + \ '&apikey=' + BRAVOS_API + '&format=json' - print(url) + + # load json data from url as an object with urllib.request.urlopen(url) as url: data = json.loads(url.read().decode()) - for ticker in tickers: - try: + for ticker in tickers: # iterate through the tickers and print relevant info one message at a time + try: # checks if data is a valid ticker, if it is not tells the user nameTicker = data[ticker.upper()]['name'] priceTicker = data[ticker.upper()]['price'] await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $' + str(priceTicker)) -- 2.51.2 From 49074984f03d9b5393aad178a766591c611dc3cb Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 8 Jan 2019 23:02:48 -0700 Subject: [PATCH 06/75] added super snazzy embeds --- .../simple-discord-stock-bot/stockBot.py | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index 311a19afd..c0699e9a1 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -5,6 +5,7 @@ import discord import re import urllib.request import credentials +from datetime import datetime # Make sure to update credentials.py with your secrets TOKEN = credentials.secrets['TOKEN'] @@ -30,24 +31,57 @@ async def on_message(message): content = message.content channel = message.channel - # regex to find tickers in messages - tickers = re.findall('[$](\w{1,4})', content) - - # get ticker information from bravos api - url = 'https://data.bravos.co/v1/quote?symbols=' + ",".join(tickers) + \ - '&apikey=' + BRAVOS_API + '&format=json' - - # load json data from url as an object - with urllib.request.urlopen(url) as url: - data = json.loads(url.read().decode()) - for ticker in tickers: # iterate through the tickers and print relevant info one message at a time - try: # checks if data is a valid ticker, if it is not tells the user - nameTicker = data[ticker.upper()]['name'] - priceTicker = data[ticker.upper()]['price'] - await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $' + str(priceTicker)) - except KeyError: - await client.send_message(channel, ticker.upper() + ' does not exist.') - pass + try: + # regex to find tickers in messages + tickers = re.findall('[$](\w{1,4})', content) + + # get ticker information from bravos api + url = 'https://data.bravos.co/v1/quote?symbols=' + ",".join(tickers) + \ + '&apikey=' + BRAVOS_API + '&format=json' + + # load json data from url as an object + with urllib.request.urlopen(url) as url: + data = json.loads(url.read().decode()) + for ticker in tickers: # iterate through the tickers and print relevant info one message at a time + try: # checks if data is a valid ticker, if it is not tells the user + nameTicker = data[ticker.upper()]['name'] + priceTicker = data[ticker.upper()]['price'] + if message.content.startswith('!news'): + embed = displayembed(ticker, nameTicker, priceTicker) + await client.send_message(channel, embed=embed) + else: + await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $**' + str(priceTicker) + '**') + except KeyError: + await client.send_message(channel, ticker.upper() + ' does not exist.') + pass + except: + pass + + +def displayembed(ticker, nameTicker, priceTicker): + + embed = discord.Embed( + title='News for ' + nameTicker, + description='The current stock price of ' + + nameTicker + ' is $**' + str(priceTicker) + '**', + color=0x50bdfe + ) + + embed.set_thumbnail( + url='https://g.foolcdn.com/art/companylogos/mark/' + ticker + '.png') + embed.add_field(name='Seeking Alpha', + value='https://seekingalpha.com/symbol/' + ticker, inline=True) + embed.add_field( + name='MSN Money', value='https://www.msn.com/en-us/money/stockdetails?symbol=' + ticker, inline=False) + embed.add_field(name='Yahoo Finance', + value='https://finance.yahoo.com/quote/' + ticker, inline=False) + embed.add_field(name='Wall Street Journal', + value='https://quotes.wsj.com/' + ticker, inline=False) + embed.add_field( + name='The Street', value='https://www.thestreet.com/quote/' + ticker + '.html', inline=False) + embed.add_field( + name='Zacks', value='https://www.zacks.com/stock/quote/' + ticker, inline=False) + return embed async def list_servers(): -- 2.51.2 From 97788025738d3e7fad696c2934cfa4b24afe6be5 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Wed, 9 Jan 2019 06:09:41 +0000 Subject: [PATCH 07/75] Add LICENSE --- .../simple-discord-stock-bot/LICENSE | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 simple-stock-bots/simple-discord-stock-bot/LICENSE diff --git a/simple-stock-bots/simple-discord-stock-bot/LICENSE b/simple-stock-bots/simple-discord-stock-bot/LICENSE new file mode 100644 index 000000000..033217724 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/LICENSE @@ -0,0 +1,173 @@ +## creative commons + +# Attribution-ShareAlike 4.0 International + +Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +### Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). + +* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). + +## Creative Commons Attribution-ShareAlike 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +### Section 1 – Definitions. + +a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + +c. __BY-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. + +d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. + +h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. + +k. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +l. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +m. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +### Section 2 – Scope. + +a. ___License grant.___ + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. __Term.__ The term of this Public License is specified in Section 6(a). + + 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. __Downstream recipients.__ + + A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. + + C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. ___Other rights.___ + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +### Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +a. ___Attribution.___ + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + +b. ___ShareAlike.___ + +In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. + +1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. + +2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. + +3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. + +### Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + +b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and + +c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +### Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ + +b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ + +c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +### Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +### Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +### Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + +c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the [CC0 Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/legalcode). Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. +> +> Creative Commons may be contacted at creativecommons.org. -- 2.51.2 From 9800b091f1bfdf43a2cc2fbef36c1a71c9f6c83e Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 9 Jan 2019 18:18:21 -0700 Subject: [PATCH 08/75] Added Bravos to news list --- simple-stock-bots/simple-discord-stock-bot/stockBot.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index c0699e9a1..9f7181e0b 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -66,11 +66,16 @@ def displayembed(ticker, nameTicker, priceTicker): nameTicker + ' is $**' + str(priceTicker) + '**', color=0x50bdfe ) - + ''' + Get ticker logo from Motley Fool, then get then print the following sources: + Bravos, Seeking Alpha, MSN Money, Yahoo Finance, Wall Street Journal, The Street. + ''' embed.set_thumbnail( url='https://g.foolcdn.com/art/companylogos/mark/' + ticker + '.png') + embed.add_field(name='Bravos', + value='https://bravos.co/' + ticker, inline=False) embed.add_field(name='Seeking Alpha', - value='https://seekingalpha.com/symbol/' + ticker, inline=True) + value='https://seekingalpha.com/symbol/' + ticker, inline=False) embed.add_field( name='MSN Money', value='https://www.msn.com/en-us/money/stockdetails?symbol=' + ticker, inline=False) embed.add_field(name='Yahoo Finance', -- 2.51.2 From d3d66636907c5c4b496be322ba4e2972e8f1d763 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 9 Jan 2019 18:19:24 -0700 Subject: [PATCH 09/75] Fixed bug where !news didn't work if capitalization wasnt perfect --- simple-stock-bots/simple-discord-stock-bot/stockBot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index 9f7181e0b..4c2a22452 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -28,7 +28,7 @@ async def on_message(message): # define information about the message author = message.author - content = message.content + content = message.content.lower() channel = message.channel try: @@ -46,7 +46,8 @@ async def on_message(message): try: # checks if data is a valid ticker, if it is not tells the user nameTicker = data[ticker.upper()]['name'] priceTicker = data[ticker.upper()]['price'] - if message.content.startswith('!news'): + + if content.startswith('!news'): embed = displayembed(ticker, nameTicker, priceTicker) await client.send_message(channel, embed=embed) else: -- 2.51.2 From ab91d4c71b8b58a2576fb50fce1fabc02002a1dc Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 9 Jan 2019 18:19:58 -0700 Subject: [PATCH 10/75] Added more comments and did some formatting --- .../simple-discord-stock-bot/stockBot.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index 4c2a22452..c3ecd6fbb 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -32,33 +32,41 @@ async def on_message(message): channel = message.channel try: - # regex to find tickers in messages + # regex to find tickers in messages, looks for up to 4 word characters following a dollar sign and captures the 4 word characters tickers = re.findall('[$](\w{1,4})', content) - # get ticker information from bravos api + # get ticker information from bravos api, turns tickers into comma separated list so that only one api call is needed per message url = 'https://data.bravos.co/v1/quote?symbols=' + ",".join(tickers) + \ '&apikey=' + BRAVOS_API + '&format=json' # load json data from url as an object with urllib.request.urlopen(url) as url: data = json.loads(url.read().decode()) + for ticker in tickers: # iterate through the tickers and print relevant info one message at a time try: # checks if data is a valid ticker, if it is not tells the user + + # Get Stock ticker name from Data Object nameTicker = data[ticker.upper()]['name'] + # Get Stock Ticker price from Object priceTicker = data[ticker.upper()]['price'] + # Checks if !news is called, and prints news embed if it is if content.startswith('!news'): + embed = displayembed(ticker, nameTicker, priceTicker) await client.send_message(channel, embed=embed) - else: + else: # If news embed isnt called, print normal stock price await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $**' + str(priceTicker) + '**') - except KeyError: + + except KeyError: # If searching for the ticker in loaded data fails, then Bravos didnt provide it, so tell the user. await client.send_message(channel, ticker.upper() + ' does not exist.') pass except: pass +# Prints an embed full of news about listed stock def displayembed(ticker, nameTicker, priceTicker): embed = discord.Embed( -- 2.51.2 From 5e2606e55d4152166b5582a2ca3cb92219bc3d2f Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 10 Jan 2019 04:03:36 +0000 Subject: [PATCH 11/75] Changed bravos link to one that points to actual API page. --- simple-stock-bots/simple-discord-stock-bot/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/credentials.py b/simple-stock-bots/simple-discord-stock-bot/credentials.py index 60f8309c3..e339fc2ae 100644 --- a/simple-stock-bots/simple-discord-stock-bot/credentials.py +++ b/simple-stock-bots/simple-discord-stock-bot/credentials.py @@ -1,4 +1,4 @@ secrets = { 'TOKEN': "https://discordapp.com/developers/applications/ > BOTNAME > Bot > 'copy token'", - 'BRAVOS_API': 'https://bravos.co/a/login' + 'BRAVOS_API': 'https://bravos.co/a/data' } -- 2.51.2 From 02bc5ae3b50d048a03999eab53f50dcfa0e9f5f8 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 9 Jan 2019 21:50:43 -0700 Subject: [PATCH 12/75] Datetime wasnt being used --- simple-stock-bots/simple-discord-stock-bot/stockBot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index c3ecd6fbb..02cf2a176 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -5,7 +5,6 @@ import discord import re import urllib.request import credentials -from datetime import datetime # Make sure to update credentials.py with your secrets TOKEN = credentials.secrets['TOKEN'] -- 2.51.2 From 776801d74db809edb823fe997c0d0969c7a42d95 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 9 Jan 2019 21:51:20 -0700 Subject: [PATCH 13/75] Made a README, you should read it. --- .../simple-discord-stock-bot/README.md | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index ce13bd170..30647fa46 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -1,5 +1,55 @@ -# Readme +# Simple Discord Stock Bot -will fill this out at some point +Super simple Discord Bot wrote in Python for displaying the price of a stock, or news about a stock quickly and easily. -*hopefully* \ No newline at end of file +## Motivation + +[Google Allo](https://blog.google/products/messages/latest-messages-allo-duo-and-hangouts/) is disappearing in March 2019 so me and my friends who use it for talking about stocks need a replacement. Allo has a feature that allows you to talk to the Google Assitant right in the chat which makes pulling up a news article or a stock quote in chat extremely easy. This bot aims to replace that specific functionality. + +## Features + +The bot checks every message for tickers which can be called by putting a dollar sign in front of the stock symbol like this: ```$tsla``` The bot is pretty smart and can handle as many tickers in a message as you put in. + +For example the message: + +```I want to know about $tsla $djia $f $aaxn $baba $amzn``` + +Will output: + +![Discord-Bot-Stock-Price-Example](https://blog.ansonbiggs.com/content/images/2019/01/Discord-Bot-Stock-Price-Example.png) + + The bot can also quickly give you news links about any tickers you need. Simply putting ```!news``` in front of your message like this: + + ```!news $tsla``` + + Will output: + +![Discord-Bot-Stock-News-Example](https://blog.ansonbiggs.com/content/images/2019/01/Discord-Bot-Stock-News-Example.png) + + The news feature also works with as many tickers as you want to put in a message at once for example: ```!news I want to know about $tsla $amzn``` + +## How it works + +The most important part of the project is [discord.py](https://github.com/Rapptz/discord.py) which is an API wrapper for Discord written in Python. discord.py made this project a breeze. + +The other important part is the [Bravos API](https://bravos.co/a/data) which is used to get the current quote for the provided ticker. The API works perfectly and is free with an account. + +The news sources are currently hardcoded, and the code simply puts the stock ticker into a URL where its needed to get a page on that specific ticker. The image of the company that is shown in the embeds are taken from Motley Fool without an official API. There's probably a better way to get the images but I couldn't find one easily. + +## Installation + +- Follow install instructions for [discord.py](https://github.com/Rapptz/discord.py#installing) without full voice support. +- Follow directions in credentials.py + - Get Discord Token [Here](https://discordapp.com/developers/applications/) > BOTNAME > Bot > 'copy token' + - Get Bravos API key [Here](https://bravos.co/a/data) after you make a free account +- Run stockBot.py + +## Contribute + +This project probably won't get additional development since I decided to use Telegram instead of Discord for group messaging, but if you would like add anything feel free to make your own clone repo, or you can contact me and maybe get a pull request going. Would also love to hear about all the things I did wrong in my code<3 + +## License + +In laymen (not a substitute for the actual license) you can do whatever you want with this project as long as you also share it, and [give me credit](https://blog.ansonbiggs.com). + +Creative Commons License
Simple Discord Stock Bot by Anson Biggs is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Based on a work at https://gitlab.com/MisterBiggs/simple-discord-stock-bot. \ No newline at end of file -- 2.51.2 From a773085f8c52cafd81a0f412565632ef0a0dada0 Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 5 Apr 2019 17:56:54 -0700 Subject: [PATCH 14/75] added docker --- simple-stock-bots/simple-discord-stock-bot/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 simple-stock-bots/simple-discord-stock-bot/Dockerfile diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile new file mode 100644 index 000000000..4c95c944b --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.6-slim + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD [ "python", "./stockBot.py" ] \ No newline at end of file -- 2.51.2 From 6416f321426cafc792eca6d4db3654d76c33a9eb Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 5 Apr 2019 17:59:03 -0700 Subject: [PATCH 15/75] cleaning up code and figuring out discord.py --- .../simple-discord-stock-bot/credentials.py | 4 - .../simple-discord-stock-bot/stockBot.py | 114 +++++------------- .../simple-discord-stock-bot/tickerInfo.py | 105 ++++++++++++++++ 3 files changed, 132 insertions(+), 91 deletions(-) delete mode 100644 simple-stock-bots/simple-discord-stock-bot/credentials.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/tickerInfo.py diff --git a/simple-stock-bots/simple-discord-stock-bot/credentials.py b/simple-stock-bots/simple-discord-stock-bot/credentials.py deleted file mode 100644 index e339fc2ae..000000000 --- a/simple-stock-bots/simple-discord-stock-bot/credentials.py +++ /dev/null @@ -1,4 +0,0 @@ -secrets = { - 'TOKEN': "https://discordapp.com/developers/applications/ > BOTNAME > Bot > 'copy token'", - 'BRAVOS_API': 'https://bravos.co/a/data' -} diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/stockBot.py index 02cf2a176..90fa66e84 100644 --- a/simple-stock-bots/simple-discord-stock-bot/stockBot.py +++ b/simple-stock-bots/simple-discord-stock-bot/stockBot.py @@ -1,107 +1,47 @@ # Work with Python 3.6 -import json -from discord.ext import commands import discord import re import urllib.request -import credentials - -# Make sure to update credentials.py with your secrets -TOKEN = credentials.secrets['TOKEN'] -BRAVOS_API = credentials.secrets['BRAVOS_API'] -BOT_PREFIX = ("?", "!") - -client = commands.Bot(command_prefix=BOT_PREFIX) +import json +import tickerInfo as ti +TOKEN = "Discord Token" +TICKER_REGEX = "[$]([a-zA-Z]{1,4})" -@client.event # Make bot say when its ready -async def on_ready(): - print('Bot is Ready!!!') +client = discord.Client() @client.event async def on_message(message): + """ + This runs every time a message is detected. + """ - if message.author == client.user: # Prevent bot from reacting to its own messages + # Check that message wasnt the bot. + if message.author == client.user: return - # define information about the message - author = message.author - content = message.content.lower() - channel = message.channel - - try: - # regex to find tickers in messages, looks for up to 4 word characters following a dollar sign and captures the 4 word characters - tickers = re.findall('[$](\w{1,4})', content) - - # get ticker information from bravos api, turns tickers into comma separated list so that only one api call is needed per message - url = 'https://data.bravos.co/v1/quote?symbols=' + ",".join(tickers) + \ - '&apikey=' + BRAVOS_API + '&format=json' - - # load json data from url as an object - with urllib.request.urlopen(url) as url: - data = json.loads(url.read().decode()) - - for ticker in tickers: # iterate through the tickers and print relevant info one message at a time - try: # checks if data is a valid ticker, if it is not tells the user - - # Get Stock ticker name from Data Object - nameTicker = data[ticker.upper()]['name'] - # Get Stock Ticker price from Object - priceTicker = data[ticker.upper()]['price'] - - # Checks if !news is called, and prints news embed if it is - if content.startswith('!news'): - - embed = displayembed(ticker, nameTicker, priceTicker) - await client.send_message(channel, embed=embed) - else: # If news embed isnt called, print normal stock price - await client.send_message(channel, 'The current stock price of ' + nameTicker + ' is $**' + str(priceTicker) + '**') - - except KeyError: # If searching for the ticker in loaded data fails, then Bravos didnt provide it, so tell the user. - await client.send_message(channel, ticker.upper() + ' does not exist.') - pass - except: - pass + tickers = re.findall(TICKER_REGEX, message.content) + if tickers is not []: + print(tickers) + await client.send_typing(message.channel) + await client.send_message(message.channel, ti.tickerMessage(tickers)) + return + # print(message.author.id) -# Prints an embed full of news about listed stock -def displayembed(ticker, nameTicker, priceTicker): + # if message.content.startswith("!hello"): + # print(dir(message.author)) + # msg = "Hello {0.author.mention}".format(message) + # await client.send_message(message.channel, msg) - embed = discord.Embed( - title='News for ' + nameTicker, - description='The current stock price of ' + - nameTicker + ' is $**' + str(priceTicker) + '**', - color=0x50bdfe - ) - ''' - Get ticker logo from Motley Fool, then get then print the following sources: - Bravos, Seeking Alpha, MSN Money, Yahoo Finance, Wall Street Journal, The Street. - ''' - embed.set_thumbnail( - url='https://g.foolcdn.com/art/companylogos/mark/' + ticker + '.png') - embed.add_field(name='Bravos', - value='https://bravos.co/' + ticker, inline=False) - embed.add_field(name='Seeking Alpha', - value='https://seekingalpha.com/symbol/' + ticker, inline=False) - embed.add_field( - name='MSN Money', value='https://www.msn.com/en-us/money/stockdetails?symbol=' + ticker, inline=False) - embed.add_field(name='Yahoo Finance', - value='https://finance.yahoo.com/quote/' + ticker, inline=False) - embed.add_field(name='Wall Street Journal', - value='https://quotes.wsj.com/' + ticker, inline=False) - embed.add_field( - name='The Street', value='https://www.thestreet.com/quote/' + ticker + '.html', inline=False) - embed.add_field( - name='Zacks', value='https://www.zacks.com/stock/quote/' + ticker, inline=False) - return embed +@client.event +async def on_ready(): + print("Logged in as") + print(client.user.name) + print(client.user.id) + print("------") -async def list_servers(): - await client.wait_until_ready() - while not client.is_closed: - print("Current servers:") - for server in client.servers: - print(server.name) client.run(TOKEN) diff --git a/simple-stock-bots/simple-discord-stock-bot/tickerInfo.py b/simple-stock-bots/simple-discord-stock-bot/tickerInfo.py new file mode 100644 index 000000000..a3d13ca8c --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/tickerInfo.py @@ -0,0 +1,105 @@ +import urllib.request +import json +from datetime import datetime +import time + + +def tickerQuote(tickers): + """Gathers information from IEX api on stock""" + stockData = {} + IEXURL = ( + "https://api.iextrading.com/1.0/stock/market/batch?symbols=" + + ",".join(tickers) + + "&types=quote" + ) + print("Gathering Quote from " + IEXURL) + with urllib.request.urlopen(IEXURL) as url: + IEXData = json.loads(url.read().decode()) + + for ticker in tickers: + ticker = ticker.upper() + + # Makes sure ticker exists before populating a dictionary + if ticker in IEXData: + stockData[ticker] = 1 + stockData[ticker + "Name"] = IEXData[ticker]["quote"]["companyName"] + stockData[ticker + "Price"] = IEXData[ticker]["quote"]["latestPrice"] + stockData[ticker + "Change"] = round( + (IEXData[ticker]["quote"]["changePercent"] * 100), 2 + ) + stockData[ticker + "Image"] = stockLogo(ticker) + print(ticker + " Quote Gathered") + else: + stockData[ticker] = 0 + return stockData + + +def stockNews(ticker): + """Makes a bunch of strings that are links to news websites for an input ticker""" + print("Gather News on " + ticker) + + newsLink = f"https://api.iextrading.com/1.0/stock/{ticker}/news/last/5" + print(newsLink) + with urllib.request.urlopen(newsLink) as url: + data = json.loads(url.read().decode()) + + news = {"link": [], "title": []} + for i in range(len(data)): + news["link"].append(data[i]["url"]) + news["title"].append(data[i]["headline"]) + return news + + +def stockLogo(ticker): + """returns a png of an input ticker""" + logoURL = f"https://g.foolcdn.com/art/companylogos/mark/{ticker}.png" + return logoURL + + +def stockInfo(ticker): + infoURL = f"https://api.iextrading.com/1.0/stock/{ticker}/stats" + + with urllib.request.urlopen(infoURL) as url: + data = json.loads(url.read().decode()) + + info = {} + + info["companyName"] = data["companyName"] + info["marketCap"] = data["marketcap"] + info["yearHigh"] = data["week52high"] + info["yearLow"] = data["week52low"] + info["divRate"] = data["dividendRate"] + info["divYield"] = data["dividendYield"] + info["divDate"] = data["exDividendDate"] + + return info + + +def stockDividend(ticker): + data = stockInfo(ticker) + print(data["divDate"]) + if data["divDate"] == 0: + return "{} has no dividend.".format(data["companyName"]) + + line1 = "{} current dividend yield is: {:.3f}%, or ${:.3f} per share.".format( + data["companyName"], data["divRate"], data["divYield"] + ) + + divDate = data["divDate"] + + # Pattern IEX uses for dividend date. + pattern = "%Y-%m-%d %H:%M:%S.%f" + + # Convert divDate to seconds, and subtract it from current time. + divSeconds = datetime.strptime(divDate, pattern).timestamp() + difference = divSeconds - int(time.time()) + + # Calculate (d)ays, (h)ours, (m)inutes, and (s)econds + d, h = divmod(difference, 86400) + h, m = divmod(h, 3600) + m, s = divmod(m, 60) + + countdownMessage = f"\n\nThe dividend is in: {d:.0f} Days {h:.0f} Hours {m:.0f} Minutes {s:.0f} Seconds." + + message = line1 + countdownMessage + return message -- 2.51.2 From 95a5ec1dbec05c51cf15d0a7fa0e29efed05c831 Mon Sep 17 00:00:00 2001 From: Anson Date: Sat, 11 May 2019 20:12:48 -0700 Subject: [PATCH 16/75] rename files to fit convention --- .../simple-discord-stock-bot/{stockBot.py => bot.py} | 0 .../simple-discord-stock-bot/{tickerInfo.py => functions.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename simple-stock-bots/simple-discord-stock-bot/{stockBot.py => bot.py} (100%) rename simple-stock-bots/simple-discord-stock-bot/{tickerInfo.py => functions.py} (100%) diff --git a/simple-stock-bots/simple-discord-stock-bot/stockBot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py similarity index 100% rename from simple-stock-bots/simple-discord-stock-bot/stockBot.py rename to simple-stock-bots/simple-discord-stock-bot/bot.py diff --git a/simple-stock-bots/simple-discord-stock-bot/tickerInfo.py b/simple-stock-bots/simple-discord-stock-bot/functions.py similarity index 100% rename from simple-stock-bots/simple-discord-stock-bot/tickerInfo.py rename to simple-stock-bots/simple-discord-stock-bot/functions.py -- 2.51.2 From 200f7f54fc4482a87d1a0c65e0a021f1ef20463d Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 9 Jun 2019 10:59:49 -0700 Subject: [PATCH 17/75] iex 2.0 functions from telegram bot --- .../simple-discord-stock-bot/functions.py | 151 ++++++++---------- 1 file changed, 63 insertions(+), 88 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index a3d13ca8c..7df749896 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -1,105 +1,80 @@ -import urllib.request import json -from datetime import datetime +import os +import re import time +import urllib.request +from datetime import datetime +IEX_TOKEN = os.environ["IEX"] -def tickerQuote(tickers): - """Gathers information from IEX api on stock""" - stockData = {} - IEXURL = ( - "https://api.iextrading.com/1.0/stock/market/batch?symbols=" - + ",".join(tickers) - + "&types=quote" - ) - print("Gathering Quote from " + IEXURL) - with urllib.request.urlopen(IEXURL) as url: - IEXData = json.loads(url.read().decode()) - - for ticker in tickers: - ticker = ticker.upper() - - # Makes sure ticker exists before populating a dictionary - if ticker in IEXData: - stockData[ticker] = 1 - stockData[ticker + "Name"] = IEXData[ticker]["quote"]["companyName"] - stockData[ticker + "Price"] = IEXData[ticker]["quote"]["latestPrice"] - stockData[ticker + "Change"] = round( - (IEXData[ticker]["quote"]["changePercent"] * 100), 2 - ) - stockData[ticker + "Image"] = stockLogo(ticker) - print(ticker + " Quote Gathered") - else: - stockData[ticker] = 0 - return stockData - - -def stockNews(ticker): - """Makes a bunch of strings that are links to news websites for an input ticker""" - print("Gather News on " + ticker) - - newsLink = f"https://api.iextrading.com/1.0/stock/{ticker}/news/last/5" - print(newsLink) - with urllib.request.urlopen(newsLink) as url: - data = json.loads(url.read().decode()) - - news = {"link": [], "title": []} - for i in range(len(data)): - news["link"].append(data[i]["url"]) - news["title"].append(data[i]["headline"]) - return news - - -def stockLogo(ticker): - """returns a png of an input ticker""" - logoURL = f"https://g.foolcdn.com/art/companylogos/mark/{ticker}.png" - return logoURL - - -def stockInfo(ticker): - infoURL = f"https://api.iextrading.com/1.0/stock/{ticker}/stats" - with urllib.request.urlopen(infoURL) as url: - data = json.loads(url.read().decode()) +def getTickers(text: str): + """ + Takes a blob of text and returns any stock tickers found. + """ - info = {} + TICKER_REGEX = "[$]([a-zA-Z]{1,4})" - info["companyName"] = data["companyName"] - info["marketCap"] = data["marketcap"] - info["yearHigh"] = data["week52high"] - info["yearLow"] = data["week52low"] - info["divRate"] = data["dividendRate"] - info["divYield"] = data["dividendYield"] - info["divDate"] = data["exDividendDate"] + return list(set(re.findall(TICKER_REGEX, text))) - return info +def tickerDataReply(tickers: list): + """ + Takes a list of tickers and returns a list of strings with information about the ticker. + """ + tickerReplies = {} + for ticker in tickers: + IEXURL = ( + f"https://cloud.iexapis.com/stable/stock/{ticker}/quote?token={IEX_TOKEN}" + ) + try: + with urllib.request.urlopen(IEXURL) as url: + IEXData = json.loads(url.read().decode()) -def stockDividend(ticker): - data = stockInfo(ticker) - print(data["divDate"]) - if data["divDate"] == 0: - return "{} has no dividend.".format(data["companyName"]) + reply = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" - line1 = "{} current dividend yield is: {:.3f}%, or ${:.3f} per share.".format( - data["companyName"], data["divRate"], data["divYield"] - ) + # Determine wording of change text + change = round(IEXData["changePercent"] * 100, 2) + if change > 0: + reply += f", the stock is currently **up {change}%**" + elif change < 0: + reply += f", the stock is currently **down {change}%**" + else: + reply += ", the stock hasn't shown any movement today." + except: + reply = f"The ticker: {ticker} was not found." - divDate = data["divDate"] + tickerReplies[ticker] = reply - # Pattern IEX uses for dividend date. - pattern = "%Y-%m-%d %H:%M:%S.%f" + return tickerReplies - # Convert divDate to seconds, and subtract it from current time. - divSeconds = datetime.strptime(divDate, pattern).timestamp() - difference = divSeconds - int(time.time()) - # Calculate (d)ays, (h)ours, (m)inutes, and (s)econds - d, h = divmod(difference, 86400) - h, m = divmod(h, 3600) - m, s = divmod(m, 60) +def tickerDividend(tickers: list): + messages = {} - countdownMessage = f"\n\nThe dividend is in: {d:.0f} Days {h:.0f} Hours {m:.0f} Minutes {s:.0f} Seconds." + for ticker in tickers: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{ticker}/dividends/next?token={IEX_TOKEN}" + with urllib.request.urlopen(IEXurl) as url: + data = json.loads(url.read().decode()) + if data: + # Pattern IEX uses for dividend date. + pattern = "%Y-%m-%d" + + # Convert divDate to seconds, and subtract it from current time. + dividendSeconds = datetime.strptime( + data["paymentDate"], pattern + ).timestamp() + difference = dividendSeconds - int(time.time()) + + # Calculate (d)ays, (h)ours, (m)inutes, and (s)econds + d, h = divmod(difference, 86400) + h, m = divmod(h, 3600) + m, s = divmod(m, 60) + + messages[ + ticker + ] = f"{data['description']}\n\nThe dividend is in: {d:.0f} Days {h:.0f} Hours {m:.0f} Minutes {s:.0f} Seconds." + else: + messages[ticker] = f"{ticker} either doesn't exist or pays no dividend." - message = line1 + countdownMessage - return message + return messages -- 2.51.2 From 24cdf670e37dc895fc9c9d96bb01e49a347df60e Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 9 Jun 2019 11:49:44 -0700 Subject: [PATCH 18/75] updated bot for iex api 2.0 and python 3.6 --- .../simple-discord-stock-bot/bot.py | 63 ++++++++----------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 90fa66e84..0b30eb76d 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,47 +1,36 @@ -# Work with Python 3.6 import discord -import re -import urllib.request -import json -import tickerInfo as ti - -TOKEN = "Discord Token" -TICKER_REGEX = "[$]([a-zA-Z]{1,4})" +import cred +from functions import * client = discord.Client() @client.event -async def on_message(message): - """ - This runs every time a message is detected. - """ - - # Check that message wasnt the bot. - if message.author == client.user: - return - - tickers = re.findall(TICKER_REGEX, message.content) - if tickers is not []: - print(tickers) - await client.send_typing(message.channel) - await client.send_message(message.channel, ti.tickerMessage(tickers)) - return - - # print(message.author.id) - - # if message.content.startswith("!hello"): - # print(dir(message.author)) - # msg = "Hello {0.author.mention}".format(message) - # await client.send_message(message.channel, msg) +async def on_ready(): + print("We have logged in as {0.user}".format(client)) @client.event -async def on_ready(): - print("Logged in as") - print(client.user.name) - print(client.user.id) - print("------") - +async def on_message(message): + if message.author == client.user: + return -client.run(TOKEN) + # Check for dividend command + if message.content.startswith("/dividend"): + replies = tickerDividend(getTickers(message.content)) + if replies: + for tick, reply in replies.items(): + await message.channel.send(reply) + else: + await message.channel.send("No tickers found.") + # If no commands, check for any tickers. + else: + replies = tickerDataReply(getTickers(message.content)) + if replies: + for symbol, reply in replies.items(): + await message.channel.send(reply) + else: + return + + +client.run(os.environ["DISCORD"]) -- 2.51.2 From 59539c71846c955a3fa815aa4a11e0fc8db3722f Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 9 Jun 2019 11:51:40 -0700 Subject: [PATCH 19/75] updated discordpy --- simple-stock-bots/simple-discord-stock-bot/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 0fe188316..359a951a6 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,2 +1 @@ -discord.py==0.16.12 -discord==0.0.2 +discord.py==1.2.1 \ No newline at end of file -- 2.51.2 From e3db620ffa7ff6fddf7c00b50e14fa329600eb62 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 9 Jun 2019 12:10:36 -0700 Subject: [PATCH 20/75] removed testing code --- simple-stock-bots/simple-discord-stock-bot/Dockerfile | 4 ++-- simple-stock-bots/simple-discord-stock-bot/bot.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile index 4c95c944b..cee81ca3d 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.6-slim +FROM python:3.7-slim COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD [ "python", "./stockBot.py" ] \ No newline at end of file +CMD [ "python", "./bot.py" ] diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 0b30eb76d..277b56c90 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,5 +1,4 @@ import discord -import cred from functions import * client = discord.Client() -- 2.51.2 From e64cebf454b17bac1f4a49244fa09b71483ba8d6 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 9 Jun 2019 14:26:34 -0700 Subject: [PATCH 21/75] updated readme to match telegram bot --- .../simple-discord-stock-bot/README.md | 121 +++++++++++++----- 1 file changed, 91 insertions(+), 30 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index 30647fa46..1f7d109b9 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -1,55 +1,116 @@ -# Simple Discord Stock Bot +

+ + Simple Discord Stock Bot +

-Super simple Discord Bot wrote in Python for displaying the price of a stock, or news about a stock quickly and easily. +

Simple Discord Stock Bot

-## Motivation +
-[Google Allo](https://blog.google/products/messages/latest-messages-allo-duo-and-hangouts/) is disappearing in March 2019 so me and my friends who use it for talking about stocks need a replacement. Allo has a feature that allows you to talk to the Google Assitant right in the chat which makes pulling up a news article or a stock quote in chat extremely easy. This bot aims to replace that specific functionality. + [![Status](https://img.shields.io/badge/status-active-success.svg)]() + [![Platform](https://img.shields.io/badge/platform-Discord-blue.svg)](https://discordapp.com/) + [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) -## Features +
-The bot checks every message for tickers which can be called by putting a dollar sign in front of the stock symbol like this: ```$tsla``` The bot is pretty smart and can handle as many tickers in a message as you put in. +--- -For example the message: +

Discord Bot 🤖 that provides Stock Market information. +
+

-```I want to know about $tsla $djia $f $aaxn $baba $amzn``` +## 📝 Table of Contents ++ [About](#about) ++ [How it works](#working) ++ [Usage](#usage) ++ [Getting Started](#getting_started) ++ [Deploying your own bot](#deployment) ++ [Built Using](#built_using) ++ [Contributing](../CONTRIBUTING.md) ++ [author](#author) ++ [Acknowledgments](#acknowledgement) -Will output: +## 🧐 About +This bot aims to be as simple as possible while providing all the information you need on the stock market. The motivation of this bot is to provide similar stock market functionality that the Google Assistant provided in [Google Allo](https://gcemetery.co/google-allo/) before the project was sunset. -![Discord-Bot-Stock-Price-Example](https://blog.ansonbiggs.com/content/images/2019/01/Discord-Bot-Stock-Price-Example.png) +## 💭 How it works - The bot can also quickly give you news links about any tickers you need. Simply putting ```!news``` in front of your message like this: +This bot works by using the [IEX API 2.0](https://iexcloud.io/docs/api/). Using various endpoints provided by the API, the bot can take either take commands from users or check any messages for stock symbols as detailed in [Usage](#usage). - ```!news $tsla``` +## 🎈 Usage - Will output: +### Basic Usage -![Discord-Bot-Stock-News-Example](https://blog.ansonbiggs.com/content/images/2019/01/Discord-Bot-Stock-News-Example.png) +The simplest way to use the bot is just by sending a message either as a direct message or in a group chat with the bot active. The bot will search every message for text with a dollar sign followed by a stock symbol, and it will return the full name of the company and the current trading price. +``` +$tsla +``` +The symbols can be anywhere in the message, and you can post as many as you like so commands such as: +``` +I wonder if $aapl is down as much as $msft is today. +``` +would return the stock price of both Apple and Microsoft like so: +``` +The current stock price of Microsoft Corp. is $131.4, the stock is currently up 2.8% - The news feature also works with as many tickers as you want to put in a message at once for example: ```!news I want to know about $tsla $amzn``` +The current stock price of Apple, Inc. is $190.15, the stock is currently up 2.66% +``` -## How it works +### /dividend -The most important part of the project is [discord.py](https://github.com/Rapptz/discord.py) which is an API wrapper for Discord written in Python. discord.py made this project a breeze. +To get information about the dividend of a stock type `/dividend` followed by any text that has symbols with a dollar sign in front of them. So, the following command: +``` +/dividend $psec +``` +Would return information about Prospect Capitals dividend: +``` +Prospect Capital Corp. Declares June 2019 Dividend of $0.06 Per Share +The dividend is in: 38 Days 3 Hours 53 Minutes 22 Seconds. +``` -The other important part is the [Bravos API](https://bravos.co/a/data) which is used to get the current quote for the provided ticker. The API works perfectly and is free with an account. +## 🏁 Getting Started +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system. -The news sources are currently hardcoded, and the code simply puts the stock ticker into a URL where its needed to get a page on that specific ticker. The image of the company that is shown in the embeds are taken from Motley Fool without an official API. There's probably a better way to get the images but I couldn't find one easily. +### Prerequisites -## Installation +This project runs neatly in a docker container, so all that you need to run it yourself is [Docker](https://hub.docker.com/?overlay=onboarding) installed on your system. -- Follow install instructions for [discord.py](https://github.com/Rapptz/discord.py#installing) without full voice support. -- Follow directions in credentials.py - - Get Discord Token [Here](https://discordapp.com/developers/applications/) > BOTNAME > Bot > 'copy token' - - Get Bravos API key [Here](https://bravos.co/a/data) after you make a free account -- Run stockBot.py +You will also need a Discord API key which can be obtained [here.](https://discordapp.com/developers/) -## Contribute +Finally, you will need and IEX Cloud API key. They offer a free tier that should be enough for any private groups, more details [here.](https://iexcloud.io/) -This project probably won't get additional development since I decided to use Telegram instead of Discord for group messaging, but if you would like add anything feel free to make your own clone repo, or you can contact me and maybe get a pull request going. Would also love to hear about all the things I did wrong in my code<3 +### Installing -## License +Once Docker is installed and you have your API keys for Discord and IEX Cloud getting the bot running on any platform is extremely easy. -In laymen (not a substitute for the actual license) you can do whatever you want with this project as long as you also share it, and [give me credit](https://blog.ansonbiggs.com). +Download or clone the repository to your machine and open a terminal in the project and build the Docker container. -Creative Commons License
Simple Discord Stock Bot by Anson Biggs is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Based on a work at https://gitlab.com/MisterBiggs/simple-discord-stock-bot. \ No newline at end of file +``` +docker build -t simple-discord-bot . +``` + +Then run the bot using your API keys. + +``` +docker run --detach \ + -e discord=DISCORD_TOKEN \ + -e IEX=IEX_TOKEN \ + simple-discord-bot +``` + +Your bot should be running! If you are new to Docker, I would recommend checking out its documentation for full control over your bot. + +## 🚀 Deploying your own bot +I recommend Digital Ocean for small projects like this because it is straightforward to use and affordable. [Sign up with my referral code, and we both get some free hosting.](https://m.do.co/c/6b5df7ef55b6) + +## ⛏️ Built Using ++ [discord.py](https://github.com/Rapptz/discord.py) - Python Discord API Wrapper ++ [Digital Ocean](https://www.digitalocean.com/) - IaaS hosting platform + +## ✍️ author ++ [Anson Biggs](https://blog.ansonbiggs.com/author/anson/) - The one and only + +## 🎉 Acknowledgements ++ Discord for having a great bot API ++ IEX Cloud for offering a free tier ++ Viewers like you ♥ -- 2.51.2 From e09a05899ae201b7d62b1dc7b8554f41eb518942 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 9 Jun 2019 14:49:16 -0700 Subject: [PATCH 22/75] added link to access hosted version of bot --- simple-stock-bots/simple-discord-stock-bot/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index 1f7d109b9..d40fbdf43 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -69,6 +69,11 @@ The dividend is in: 38 Days 3 Hours 53 Minutes 22 Seconds. ``` ## 🏁 Getting Started + +You can either choose to use the hosted version of the bot by [clicking here](https://discordapp.com/oauth2/authorize?client_id=532045200823025666&permissions=2048&scope=bot) or you can host your own bot with the instructions below. + +### Self Hosted Bot + These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system. ### Prerequisites -- 2.51.2 From c3eb44b3cfa24c7360a5afca3c8194d53a4f4bc5 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 11 Jun 2019 05:32:59 -0700 Subject: [PATCH 23/75] added help link --- simple-stock-bots/simple-discord-stock-bot/bot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 277b56c90..3407ed318 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -22,6 +22,10 @@ async def on_message(message): await message.channel.send(reply) else: await message.channel.send("No tickers found.") + elif message.content.startswith('/help'): + """Send link to docs when the command /help is issued.""" + message = "[Please see the docs for Bot information](https://simple-stock-bots.gitlab.io/site/discord/)" + await message.channel.send(message) # If no commands, check for any tickers. else: replies = tickerDataReply(getTickers(message.content)) -- 2.51.2 From 2c1f7447096b054605c9322b1bfa8a5310cfa11e Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 11 Jun 2019 05:37:10 -0700 Subject: [PATCH 24/75] ported news and info commands from telegram bot --- .../simple-discord-stock-bot/bot.py | 18 ++++++++ .../simple-discord-stock-bot/functions.py | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 3407ed318..ab9415066 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -22,10 +22,28 @@ async def on_message(message): await message.channel.send(reply) else: await message.channel.send("No tickers found.") + + elif message.content.startswith("/news"): + replies = tickerNews(getTickers(message.content)) + if replies: + for tick, reply in replies.items(): + await message.channel.send(reply) + else: + await message.channel.send("No tickers found.") + + elif message.content.startswith("/info"): + replies = tickerInfo(getTickers(message.content)) + if replies: + for tick, reply in replies.items(): + await message.channel.send(reply) + else: + await message.channel.send("No tickers found.") + elif message.content.startswith('/help'): """Send link to docs when the command /help is issued.""" message = "[Please see the docs for Bot information](https://simple-stock-bots.gitlab.io/site/discord/)" await message.channel.send(message) + # If no commands, check for any tickers. else: replies = tickerDataReply(getTickers(message.content)) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 7df749896..fef6fd3ea 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -78,3 +78,46 @@ def tickerDividend(tickers: list): messages[ticker] = f"{ticker} either doesn't exist or pays no dividend." return messages + + +def tickerNews(tickers: list): + messages = {} + + for ticker in tickers: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{ticker}/news/last/3?token={IEX_TOKEN}" + with urllib.request.urlopen(IEXurl) as url: + data = json.loads(url.read().decode()) + if data: + messages[ticker] = f"News for **{ticker.upper()}**:\n" + for news in data: + message = f"\t[{news['headline']}]({news['url']})\n\n" + messages[ticker] = messages[ticker] + message + else: + messages[ + ticker + ] = f"No news found for: {ticker}\nEither today is boring or the ticker does not exist." + + return messages + + +def tickerInfo(tickers: list): + messages = {} + + for ticker in tickers: + IEXurl = ( + f"https://cloud.iexapis.com/stable/stock/{ticker}/company?token={IEX_TOKEN}" + ) + with urllib.request.urlopen(IEXurl) as url: + data = json.loads(url.read().decode()) + if data: + messages[ + ticker + ] = f"Company Name: [{data['companyName']}]({data['website']})\nIndustry: {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" + + else: + messages[ + ticker + ] = f"No information found for: {ticker}\nEither today is boring or the ticker does not exist." + + return messages + -- 2.51.2 From ae9d98955a41e66567c2566614235d92f670e098 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 11 Jun 2019 05:38:27 -0700 Subject: [PATCH 25/75] updated documentation --- .../simple-discord-stock-bot/README.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index d40fbdf43..d20aa8dd9 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -68,6 +68,38 @@ Prospect Capital Corp. Declares June 2019 Dividend of $0.06 Per Share The dividend is in: 38 Days 3 Hours 53 Minutes 22 Seconds. ``` +💡 you can also call the dividend command using /div + +### /news + +To get the latest news about a stock symbol use `/news` followed by any text that has symbols with a dollar sign in front of them. So, the following command: +``` +/news $psec +``` +Would return news for Prospect Capital: + +News for PSEC: + + [Yield-Starved Investors Still Accumulating BDCs Paying More Than 10% Annually](https://cloud.iexapis.com/v1/news/article/d994b8b5-9fbf-4ceb-afbe-e6defcfc6352) + + [Assessing Main Street Capital's Results For Q1 2019 (Includes Updated Price Target And Investment Ratings Analysis)](https://cloud.iexapis.com/v1/news/article/e60899bc-5230-4388-a609-fc2b8736a7d4) + + [Fully Assessing Prospect Capital's Fiscal Q3 2019 (Includes Current Recommendation And Price Target)](https://cloud.iexapis.com/v1/news/article/08881160-72c5-4f5d-885b-1751187d24eb) + +### /info + +To get information about a stock type `/info` followed by any text that has symbols with a dollar sign in front of them. So, the following command: +``` +/info $psec +``` +Would return information about Prospect Capitals: + +Company Name: [Prospect Capital Corp.](http://www.prospectstreet.com/) +Industry: Investment Managers +Sector: Finance +CEO: John Francis Barry +Description: Prospect Capital Corp. is a business development company, which engages in lending to and investing in private businesses. It also involves in generating current income and long-term capital appreciation through debt and equity investments. The company was founded on April 13, 2004 and is headquartered in New York, NY. + ## 🏁 Getting Started You can either choose to use the hosted version of the bot by [clicking here](https://discordapp.com/oauth2/authorize?client_id=532045200823025666&permissions=2048&scope=bot) or you can host your own bot with the instructions below. -- 2.51.2 From 8b38ce67028735b06db7e16e775a3ed7de1ae709 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jul 2019 13:20:52 -0700 Subject: [PATCH 26/75] copied updates from telegram bot --- .../simple-discord-stock-bot/functions.py | 147 +++++++++--------- 1 file changed, 75 insertions(+), 72 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index fef6fd3ea..5ddc340bf 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -1,123 +1,126 @@ import json import os import re -import time -import urllib.request from datetime import datetime +import requests + IEX_TOKEN = os.environ["IEX"] -def getTickers(text: str): +def getSymbols(text: str): """ - Takes a blob of text and returns any stock tickers found. + Takes a blob of text and returns a list of symbols without any repeats. """ - TICKER_REGEX = "[$]([a-zA-Z]{1,4})" + SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" - return list(set(re.findall(TICKER_REGEX, text))) + return list(set(re.findall(SYMBOL_REGEX, text))) -def tickerDataReply(tickers: list): +def symbolDataReply(symbols: list): """ - Takes a list of tickers and returns a list of strings with information about the ticker. + Takes a list of symbols and returns a dictionary of strings with information about the symbol. """ - tickerReplies = {} - for ticker in tickers: - IEXURL = ( - f"https://cloud.iexapis.com/stable/stock/{ticker}/quote?token={IEX_TOKEN}" + dataMessages = {} + for symbol in symbols: + IEXurl = ( + f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={IEX_TOKEN}" ) - try: - with urllib.request.urlopen(IEXURL) as url: - IEXData = json.loads(url.read().decode()) - reply = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" + response = requests.get(IEXurl) + if response.status_code is 200: + IEXData = response.json() + message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" # Determine wording of change text change = round(IEXData["changePercent"] * 100, 2) if change > 0: - reply += f", the stock is currently **up {change}%**" + message += f", the stock is currently **up {change}%**" elif change < 0: - reply += f", the stock is currently **down {change}%**" + message += f", the stock is currently **down {change}%**" else: - reply += ", the stock hasn't shown any movement today." - except: - reply = f"The ticker: {ticker} was not found." + message += ", the stock hasn't shown any movement today." + else: + message = f"The symbol: {symbol} was not found." + + dataMessages[symbol] = message - tickerReplies[ticker] = reply + return dataMessages - return tickerReplies +def symbolDividend(symbols: list): + divMessages = {} -def tickerDividend(tickers: list): - messages = {} + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/data-points/{symbol}/NEXTDIVIDENDDATE?token={IEX_TOKEN}" + response = requests.get(IEXurl) + if response.status_code is 200: - for ticker in tickers: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{ticker}/dividends/next?token={IEX_TOKEN}" - with urllib.request.urlopen(IEXurl) as url: - data = json.loads(url.read().decode()) - if data: + # extract date from json + date = response.json() # Pattern IEX uses for dividend date. pattern = "%Y-%m-%d" + divDate = datetime.strptime(date, pattern) + + daysDelta = (divDate - datetime.now()).days + datePretty = divDate.strftime("%A, %B %w") + if daysDelta < 0: + divMessages[ + symbol + ] = f"{symbol.upper()} dividend was on {datePretty} and a new date hasn't been announced yet." + elif daysDelta > 0: + divMessages[ + symbol + ] = f"{symbol.upper()} dividend is on {datePretty} which is in {daysDelta} Days." + else: + divMessages[symbol] = f"{symbol.upper()} is today." - # Convert divDate to seconds, and subtract it from current time. - dividendSeconds = datetime.strptime( - data["paymentDate"], pattern - ).timestamp() - difference = dividendSeconds - int(time.time()) - - # Calculate (d)ays, (h)ours, (m)inutes, and (s)econds - d, h = divmod(difference, 86400) - h, m = divmod(h, 3600) - m, s = divmod(m, 60) - - messages[ - ticker - ] = f"{data['description']}\n\nThe dividend is in: {d:.0f} Days {h:.0f} Hours {m:.0f} Minutes {s:.0f} Seconds." else: - messages[ticker] = f"{ticker} either doesn't exist or pays no dividend." + divMessages[symbol] = f"{symbol} either doesn't exist or pays no dividend." - return messages + return divMessages -def tickerNews(tickers: list): - messages = {} +def symbolNews(symbols: list): + newsMessages = {} - for ticker in tickers: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{ticker}/news/last/3?token={IEX_TOKEN}" - with urllib.request.urlopen(IEXurl) as url: - data = json.loads(url.read().decode()) - if data: - messages[ticker] = f"News for **{ticker.upper()}**:\n" + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/3?token={IEX_TOKEN}" + response = requests.get(IEXurl) + if response.status_code is 200: + data = response.json() + newsMessages[symbol] = f"News for **{symbol.upper()}**:\n" for news in data: message = f"\t[{news['headline']}]({news['url']})\n\n" - messages[ticker] = messages[ticker] + message + newsMessages[symbol] = newsMessages[symbol] + message else: - messages[ - ticker - ] = f"No news found for: {ticker}\nEither today is boring or the ticker does not exist." + newsMessages[ + symbol + ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." - return messages + return newsMessages -def tickerInfo(tickers: list): - messages = {} +def symbolInfo(symbols: list): + infoMessages = {} - for ticker in tickers: + for symbol in symbols: IEXurl = ( - f"https://cloud.iexapis.com/stable/stock/{ticker}/company?token={IEX_TOKEN}" + f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={IEX_TOKEN}" ) - with urllib.request.urlopen(IEXurl) as url: - data = json.loads(url.read().decode()) - if data: - messages[ - ticker + response = requests.get(IEXurl) + + if response.status_code is 200: + data = response.json() + infoMessages[ + symbol ] = f"Company Name: [{data['companyName']}]({data['website']})\nIndustry: {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" else: - messages[ - ticker - ] = f"No information found for: {ticker}\nEither today is boring or the ticker does not exist." + infoMessages[ + symbol + ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - return messages + return infoMessages -- 2.51.2 From d05f2d790f0225e70d4ba5bcf61297bb96e48628 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jul 2019 13:32:26 -0700 Subject: [PATCH 27/75] finished importing new functions code --- .../simple-discord-stock-bot/bot.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index ab9415066..f46fb536e 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -16,40 +16,40 @@ async def on_message(message): # Check for dividend command if message.content.startswith("/dividend"): - replies = tickerDividend(getTickers(message.content)) + replies = symbolDividend(getSymbols(message.content)) if replies: - for tick, reply in replies.items(): - await message.channel.send(reply) + for reply in replies.items(): + await message.channel.send(reply[1]) else: await message.channel.send("No tickers found.") elif message.content.startswith("/news"): - replies = tickerNews(getTickers(message.content)) + replies = symbolNews(getSymbols(message.content)) if replies: - for tick, reply in replies.items(): - await message.channel.send(reply) + for reply in replies.items(): + await message.channel.send(reply[1]) else: await message.channel.send("No tickers found.") elif message.content.startswith("/info"): - replies = tickerInfo(getTickers(message.content)) + replies = symbolInfo(getSymbols(message.content)) if replies: - for tick, reply in replies.items(): - await message.channel.send(reply) + for reply in replies.items(): + await message.channel.send(reply[1]) else: await message.channel.send("No tickers found.") - elif message.content.startswith('/help'): + elif message.content.startswith("/help"): """Send link to docs when the command /help is issued.""" - message = "[Please see the docs for Bot information](https://simple-stock-bots.gitlab.io/site/discord/)" + reply = "[Please see the documentation for Bot information](https://simple-stock-bots.gitlab.io/site/discord/)" await message.channel.send(message) # If no commands, check for any tickers. else: - replies = tickerDataReply(getTickers(message.content)) + replies = symbolDataReply(getSymbols(message.content)) if replies: - for symbol, reply in replies.items(): - await message.channel.send(reply) + for reply in replies.items(): + await message.channel.send(reply[1]) else: return -- 2.51.2 From 805398c7b2c81e2aa70adafa6ba5703fab81ab34 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jul 2019 13:44:33 -0700 Subject: [PATCH 28/75] updated requirements for new code --- simple-stock-bots/simple-discord-stock-bot/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 359a951a6..fff930437 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1 +1,2 @@ -discord.py==1.2.1 \ No newline at end of file +discord.py==1.2.1 +requests==2.21.0 \ No newline at end of file -- 2.51.2 From 473f1ab6c3dcce4ef573fde1513dc399cc8c7419 Mon Sep 17 00:00:00 2001 From: Joshua Cleek Date: Sun, 7 Jun 2020 22:09:09 +0000 Subject: [PATCH 29/75] Update README.md --- simple-stock-bots/simple-discord-stock-bot/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index d20aa8dd9..c4f4ea2d2 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -130,7 +130,7 @@ Then run the bot using your API keys. ``` docker run --detach \ - -e discord=DISCORD_TOKEN \ + -e DISCORD=DISCORD_TOKEN \ -e IEX=IEX_TOKEN \ simple-discord-bot ``` -- 2.51.2 From 339a1b2012f96c89cfaa56d3aa9fffd0177da45c Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jun 2020 22:22:57 -0700 Subject: [PATCH 30/75] updated packages --- simple-stock-bots/simple-discord-stock-bot/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index fff930437..86bf624f3 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,2 +1,2 @@ -discord.py==1.2.1 -requests==2.21.0 \ No newline at end of file +discord.py==1.3.3 +requests==2.23.0 \ No newline at end of file -- 2.51.2 From b7ddf57cc8eda22edce87239e815e9b2dc90e81a Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jun 2020 22:24:31 -0700 Subject: [PATCH 31/75] imported functions from telegram bot --- .../simple-discord-stock-bot/functions.py | 281 ++++++++++++------ 1 file changed, 184 insertions(+), 97 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 5ddc340bf..6c9e4320b 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -1,126 +1,213 @@ import json import os import re -from datetime import datetime +from datetime import datetime, timedelta -import requests +import pandas as pd +import requests as r +from fuzzywuzzy import fuzz +import schedule -IEX_TOKEN = os.environ["IEX"] - -def getSymbols(text: str): +class Symbol: """ - Takes a blob of text and returns a list of symbols without any repeats. + Functions for finding stock market information about symbols. """ SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" - return list(set(re.findall(SYMBOL_REGEX, text))) - + searched_symbols = {} -def symbolDataReply(symbols: list): - """ - Takes a list of symbols and returns a dictionary of strings with information about the symbol. - """ - dataMessages = {} - for symbol in symbols: - IEXurl = ( - f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={IEX_TOKEN}" - ) + help_text = """ +Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) - response = requests.get(IEXurl) - if response.status_code is 200: - IEXData = response.json() - message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" - - # Determine wording of change text - change = round(IEXData["changePercent"] * 100, 2) - if change > 0: - message += f", the stock is currently **up {change}%**" - elif change < 0: - message += f", the stock is currently **down {change}%**" - else: - message += ", the stock hasn't shown any movement today." - else: - message = f"The symbol: {symbol} was not found." +Full documentation can be found [here.](https://simple-stock-bots.gitlab.io/site/telegram/) - dataMessages[symbol] = message +**Commands** + - /dividend `$[symbol]` will return dividend information for the symbol. + - /news `$[symbol]` will return news about the symbol. + - /info `$[symbol]` will return general information about the symbol. - return dataMessages +**Inline Features** + You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. +The bot also looks at every message in any chat it is in for stock symbols. Symbols start with a `$` followed by the stock symbol. For example: $tsla would return price information for Tesla Motors. -def symbolDividend(symbols: list): - divMessages = {} +Market data is provided by [IEX Cloud](https://iexcloud.io) + """ - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/data-points/{symbol}/NEXTDIVIDENDDATE?token={IEX_TOKEN}" - response = requests.get(IEXurl) - if response.status_code is 200: + def __init__(self, IEX_TOKEN: str): + self.IEX_TOKEN = IEX_TOKEN + self.get_symbol_list() + schedule.every().day.do(self.get_symbol_list) + + def get_symbol_list(self, return_df=False): + """ + Fetches a list of stock market symbols from FINRA + + Returns: + pd.DataFrame -- [DataFrame with columns: Symbol | Issue_Name | Primary_Listing_Mkt + datetime -- The time when the list of symbols was fetched. The Symbol list is updated every open and close of every trading day. + """ + raw_symbols = r.get( + f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" + ).json() + symbols = pd.DataFrame(data=raw_symbols) + + symbols["description"] = symbols["symbol"] + ": " + symbols["name"] + self.symbol_list = symbols + if return_df: + return symbols, datetime.now() + + def search_symbols(self, search: str): + """ + Performs a fuzzy search to find stock symbols closest to a search term. + + Arguments: + search {str} -- String used to search, could be a company name or something close to the companies stock ticker. + + Returns: + List of Tuples -- A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + """ + schedule.run_pending() + search = search.lower() + try: # https://stackoverflow.com/a/3845776/8774114 + return self.searched_symbols[search] + except KeyError: + pass + + symbols = self.symbol_list + symbols["Match"] = symbols.apply( + lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), axis=1, + ) - # extract date from json - date = response.json() - # Pattern IEX uses for dividend date. - pattern = "%Y-%m-%d" - divDate = datetime.strptime(date, pattern) + symbols.sort_values(by="Match", ascending=False, inplace=True) + if symbols["Match"].head().sum() < 300: + symbols["Match"] = symbols.apply( + lambda x: fuzz.partial_ratio(search, x["name"].lower()), axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + symbols = symbols.head(10) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def find_symbols(self, text: str): + """ + Finds stock tickers starting with a dollar sign in a blob of text and returns them in a list. Only returns each match once. Example: Whats the price of $tsla? -> ['tsla'] + + Arguments: + text {str} -- Blob of text that might contain tickers with the format: $TICKER + + Returns: + list -- List of every found match without the dollar sign. + """ + + return list(set(re.findall(self.SYMBOL_REGEX, text))) + + def price_reply(self, symbols: list): + """ + Takes a list of symbols and replies with Markdown formatted text about the symbols price change for the day. + + Arguments: + symbols {list} -- List of stock market symbols. + + Returns: + dict -- Dictionary with keys of symbols and values of markdown formatted text example: {'tsla': 'The current stock price of Tesla Motors is $**420$$, the stock price is currently **up 42%**} + """ + dataMessages = {} + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={self.IEX_TOKEN}" + + response = r.get(IEXurl) + if response.status_code == 200: + IEXData = response.json() + message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" + # Determine wording of change text + change = round(IEXData["changePercent"] * 100, 2) + if change > 0: + message += f", the stock is currently **up {change}%**" + elif change < 0: + message += f", the stock is currently **down {change}%**" + else: + message += ", the stock hasn't shown any movement today." + else: + message = f"The symbol: {symbol} was not found." + + dataMessages[symbol] = message + + return dataMessages + + def dividend_reply(self, symbols: list): + divMessages = {} + + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/data-points/{symbol}/NEXTDIVIDENDDATE?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200: + + # extract date from json + date = response.json() + # Pattern IEX uses for dividend date. + pattern = "%Y-%m-%d" + divDate = datetime.strptime(date, pattern) + + daysDelta = (divDate - datetime.now()).days + datePretty = divDate.strftime("%A, %B %w") + if daysDelta < 0: + divMessages[ + symbol + ] = f"{symbol.upper()} dividend was on {datePretty} and a new date hasn't been announced yet." + elif daysDelta > 0: + divMessages[ + symbol + ] = f"{symbol.upper()} dividend is on {datePretty} which is in {daysDelta} Days." + else: + divMessages[symbol] = f"{symbol.upper()} is today." - daysDelta = (divDate - datetime.now()).days - datePretty = divDate.strftime("%A, %B %w") - if daysDelta < 0: - divMessages[ - symbol - ] = f"{symbol.upper()} dividend was on {datePretty} and a new date hasn't been announced yet." - elif daysDelta > 0: + else: divMessages[ symbol - ] = f"{symbol.upper()} dividend is on {datePretty} which is in {daysDelta} Days." + ] = f"{symbol} either doesn't exist or pays no dividend." + + return divMessages + + def news_reply(self, symbols: list): + newsMessages = {} + + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/3?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200: + data = response.json() + newsMessages[symbol] = f"News for **{symbol.upper()}**:\n" + for news in data: + message = f"\t[{news['headline']}]({news['url']})\n\n" + newsMessages[symbol] = newsMessages[symbol] + message else: - divMessages[symbol] = f"{symbol.upper()} is today." - - else: - divMessages[symbol] = f"{symbol} either doesn't exist or pays no dividend." - - return divMessages - - -def symbolNews(symbols: list): - newsMessages = {} - - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/3?token={IEX_TOKEN}" - response = requests.get(IEXurl) - if response.status_code is 200: - data = response.json() - newsMessages[symbol] = f"News for **{symbol.upper()}**:\n" - for news in data: - message = f"\t[{news['headline']}]({news['url']})\n\n" - newsMessages[symbol] = newsMessages[symbol] + message - else: - newsMessages[ - symbol - ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." - - return newsMessages - + newsMessages[ + symbol + ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." -def symbolInfo(symbols: list): - infoMessages = {} + return newsMessages - for symbol in symbols: - IEXurl = ( - f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={IEX_TOKEN}" - ) - response = requests.get(IEXurl) + def info_reply(self, symbols: list): + infoMessages = {} - if response.status_code is 200: - data = response.json() - infoMessages[ - symbol - ] = f"Company Name: [{data['companyName']}]({data['website']})\nIndustry: {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" + response = r.get(IEXurl) - else: - infoMessages[ - symbol - ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + if response.status_code == 200: + data = response.json() + infoMessages[ + symbol + ] = f"Company Name: [{data['companyName']}]({data['website']})\nIndustry: {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" - return infoMessages + else: + infoMessages[ + symbol + ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + return infoMessages -- 2.51.2 From 4b968b46bc1df0f74abd6df08e480221bb375541 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jun 2020 23:23:01 -0700 Subject: [PATCH 32/75] updated bot with new features function code. --- .../simple-discord-stock-bot/.gitignore | 2 + .../simple-discord-stock-bot/bot.py | 40 ++++++++++++++----- .../simple-discord-stock-bot/functions.py | 4 +- 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 simple-stock-bots/simple-discord-stock-bot/.gitignore diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitignore b/simple-stock-bots/simple-discord-stock-bot/.gitignore new file mode 100644 index 000000000..be952f709 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/.gitignore @@ -0,0 +1,2 @@ +.vscode/settings.json +__pycache__/functions.cpython-38.pyc diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index f46fb536e..aefd2c079 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,7 +1,9 @@ import discord -from functions import * +from functions import Symbol +import os client = discord.Client() +s = Symbol(os.environ["IEX"]) @client.event @@ -16,37 +18,57 @@ async def on_message(message): # Check for dividend command if message.content.startswith("/dividend"): - replies = symbolDividend(getSymbols(message.content)) + replies = s.dividend_reply(s.find_symbols(message.content)) if replies: for reply in replies.items(): await message.channel.send(reply[1]) else: - await message.channel.send("No tickers found.") + + await message.channel.send( + "Command requires a ticker. See /help for more information." + ) elif message.content.startswith("/news"): - replies = symbolNews(getSymbols(message.content)) + replies = s.news_reply(s.find_symbols(message.content)) if replies: for reply in replies.items(): await message.channel.send(reply[1]) else: - await message.channel.send("No tickers found.") + await message.channel.send( + "Command requires a ticker. See /help for more information." + ) elif message.content.startswith("/info"): - replies = symbolInfo(getSymbols(message.content)) + replies = s.info_reply(s.find_symbols(message.content)) if replies: for reply in replies.items(): await message.channel.send(reply[1]) else: - await message.channel.send("No tickers found.") + await message.channel.send( + "Command requires a ticker. See /help for more information." + ) + + elif message.content.startswith("/search"): + queries = s.search_symbols(message.content[7:])[:6] + if queries: + reply = "*Search Results:*\n`$ticker: Company Name`\n" + for query in queries: + reply += "`" + query[1] + "`\n" + await message.channel.send(reply) + + else: + await message.channel.send( + "Command requires a ticker. See /help for more information." + ) elif message.content.startswith("/help"): """Send link to docs when the command /help is issued.""" reply = "[Please see the documentation for Bot information](https://simple-stock-bots.gitlab.io/site/discord/)" - await message.channel.send(message) + await message.channel.send(s.help_text) # If no commands, check for any tickers. else: - replies = symbolDataReply(getSymbols(message.content)) + replies = s.price_reply(s.find_symbols(message.content)) if replies: for reply in replies.items(): await message.channel.send(reply[1]) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 6c9e4320b..30cf834e1 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -1,5 +1,4 @@ import json -import os import re from datetime import datetime, timedelta @@ -27,13 +26,14 @@ Full documentation can be found [here.](https://simple-stock-bots.gitlab.io/site - /dividend `$[symbol]` will return dividend information for the symbol. - /news `$[symbol]` will return news about the symbol. - /info `$[symbol]` will return general information about the symbol. + - /search `query` Takes a search string, whether a company name or ticker and returns a list of companies that are supported by the bot. **Inline Features** You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. The bot also looks at every message in any chat it is in for stock symbols. Symbols start with a `$` followed by the stock symbol. For example: $tsla would return price information for Tesla Motors. -Market data is provided by [IEX Cloud](https://iexcloud.io) +`Market data is provided by [IEX Cloud](https://iexcloud.io)` """ def __init__(self, IEX_TOKEN: str): -- 2.51.2 From fc7e5188ee9f8e820c2e3a429ba126263f9abfc9 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jun 2020 23:48:55 -0700 Subject: [PATCH 33/75] added rest of new dependencies from new code --- simple-stock-bots/simple-discord-stock-bot/Dockerfile | 2 +- simple-stock-bots/simple-discord-stock-bot/requirements.txt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile index cee81ca3d..eb3699110 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-slim +FROM python:3.8-buster COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 86bf624f3..53663460b 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,2 +1,6 @@ discord.py==1.3.3 -requests==2.23.0 \ No newline at end of file +requests==2.23.0 +pandas==1.0.3 +fuzzywuzzy==0.18.0 +python-Levenshtein==0.12.0 +schedule==0.6.0 \ No newline at end of file -- 2.51.2 From 08ccb6ed51ad517c3d52cb6d5fe490d08c336a8e Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 10 Jun 2020 23:56:37 -0700 Subject: [PATCH 34/75] formatting and docs on new commands --- .../simple-discord-stock-bot/README.md | 81 ++++++++++++------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index d20aa8dd9..14418a958 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -1,3 +1,4 @@ +

Simple Discord Stock Bot @@ -5,11 +6,11 @@

Simple Discord Stock Bot

-
+Buy Me A Beer - [![Status](https://img.shields.io/badge/status-active-success.svg)]() - [![Platform](https://img.shields.io/badge/platform-Discord-blue.svg)](https://discordapp.com/) - [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) +[![Status](https://img.shields.io/badge/status-active-success.svg)]() +[![Platform](https://img.shields.io/badge/platform-Discord-blue.svg)]() +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE)
@@ -20,17 +21,19 @@

## 📝 Table of Contents -+ [About](#about) -+ [How it works](#working) -+ [Usage](#usage) -+ [Getting Started](#getting_started) -+ [Deploying your own bot](#deployment) -+ [Built Using](#built_using) -+ [Contributing](../CONTRIBUTING.md) -+ [author](#author) -+ [Acknowledgments](#acknowledgement) + +- [About](#about) +- [How it works](#working) +- [Usage](#usage) +- [Getting Started](#getting_started) +- [Deploying your own bot](#deployment) +- [Built Using](#built_using) +- [Contributing](../CONTRIBUTING.md) +- [author](#author) +- [Acknowledgments](#acknowledgement) ## 🧐 About + This bot aims to be as simple as possible while providing all the information you need on the stock market. The motivation of this bot is to provide similar stock market functionality that the Google Assistant provided in [Google Allo](https://gcemetery.co/google-allo/) before the project was sunset. ## 💭 How it works @@ -42,56 +45,72 @@ This bot works by using the [IEX API 2.0](https://iexcloud.io/docs/api/). Using ### Basic Usage The simplest way to use the bot is just by sending a message either as a direct message or in a group chat with the bot active. The bot will search every message for text with a dollar sign followed by a stock symbol, and it will return the full name of the company and the current trading price. + ``` $tsla ``` + The symbols can be anywhere in the message, and you can post as many as you like so commands such as: + ``` I wonder if $aapl is down as much as $msft is today. ``` + would return the stock price of both Apple and Microsoft like so: + ``` The current stock price of Microsoft Corp. is $131.4, the stock is currently up 2.8% The current stock price of Apple, Inc. is $190.15, the stock is currently up 2.66% ``` -### /dividend +### /search `query` + +This command takes any query and searches all tickers and company names that the bot supports and returns matches. The bot uses IEX Cloud so market information is limited to what they support, which is still about ~15,000 tickers so anything in a United States exchange should have support. + +### /dividend \$ticker To get information about the dividend of a stock type `/dividend` followed by any text that has symbols with a dollar sign in front of them. So, the following command: + ``` /dividend $psec ``` + Would return information about Prospect Capitals dividend: + ``` -Prospect Capital Corp. Declares June 2019 Dividend of $0.06 Per Share +Prospect Capital Corp. Declares June 2019 Dividend of $0.06 Per Share The dividend is in: 38 Days 3 Hours 53 Minutes 22 Seconds. ``` 💡 you can also call the dividend command using /div -### /news +### /news \$ticker To get the latest news about a stock symbol use `/news` followed by any text that has symbols with a dollar sign in front of them. So, the following command: + ``` /news $psec ``` + Would return news for Prospect Capital: News for PSEC: - [Yield-Starved Investors Still Accumulating BDCs Paying More Than 10% Annually](https://cloud.iexapis.com/v1/news/article/d994b8b5-9fbf-4ceb-afbe-e6defcfc6352) +[Yield-Starved Investors Still Accumulating BDCs Paying More Than 10% Annually](https://cloud.iexapis.com/v1/news/article/d994b8b5-9fbf-4ceb-afbe-e6defcfc6352) - [Assessing Main Street Capital's Results For Q1 2019 (Includes Updated Price Target And Investment Ratings Analysis)](https://cloud.iexapis.com/v1/news/article/e60899bc-5230-4388-a609-fc2b8736a7d4) +[Assessing Main Street Capital's Results For Q1 2019 (Includes Updated Price Target And Investment Ratings Analysis)](https://cloud.iexapis.com/v1/news/article/e60899bc-5230-4388-a609-fc2b8736a7d4) - [Fully Assessing Prospect Capital's Fiscal Q3 2019 (Includes Current Recommendation And Price Target)](https://cloud.iexapis.com/v1/news/article/08881160-72c5-4f5d-885b-1751187d24eb) +[Fully Assessing Prospect Capital's Fiscal Q3 2019 (Includes Current Recommendation And Price Target)](https://cloud.iexapis.com/v1/news/article/08881160-72c5-4f5d-885b-1751187d24eb) -### /info +### /info \$ticker To get information about a stock type `/info` followed by any text that has symbols with a dollar sign in front of them. So, the following command: + ``` /info $psec ``` + Would return information about Prospect Capitals: Company Name: [Prospect Capital Corp.](http://www.prospectstreet.com/) @@ -102,7 +121,7 @@ Description: Prospect Capital Corp. is a business development company, which eng ## 🏁 Getting Started -You can either choose to use the hosted version of the bot by [clicking here](https://discordapp.com/oauth2/authorize?client_id=532045200823025666&permissions=2048&scope=bot) or you can host your own bot with the instructions below. +You can either choose to use the hosted version of the bot by [clicking here](https://discordapp.com/oauth2/authorize?client_id=532045200823025666&permissions=2048&scope=bot) or you can host your own bot with the instructions below. ### Self Hosted Bot @@ -118,7 +137,7 @@ Finally, you will need and IEX Cloud API key. They offer a free tier that should ### Installing -Once Docker is installed and you have your API keys for Discord and IEX Cloud getting the bot running on any platform is extremely easy. +Once Docker is installed and you have your API keys for Discord and IEX Cloud getting the bot running on any platform is extremely easy. Download or clone the repository to your machine and open a terminal in the project and build the Docker container. @@ -135,19 +154,23 @@ docker run --detach \ simple-discord-bot ``` -Your bot should be running! If you are new to Docker, I would recommend checking out its documentation for full control over your bot. +Your bot should be running! If you are new to Docker, I would recommend checking out its documentation for full control over your bot. ## 🚀 Deploying your own bot + I recommend Digital Ocean for small projects like this because it is straightforward to use and affordable. [Sign up with my referral code, and we both get some free hosting.](https://m.do.co/c/6b5df7ef55b6) ## ⛏️ Built Using -+ [discord.py](https://github.com/Rapptz/discord.py) - Python Discord API Wrapper -+ [Digital Ocean](https://www.digitalocean.com/) - IaaS hosting platform + +- [discord.py](https://github.com/Rapptz/discord.py) - Python Discord API Wrapper +- [Digital Ocean](https://www.digitalocean.com/) - IaaS hosting platform ## ✍️ author -+ [Anson Biggs](https://blog.ansonbiggs.com/author/anson/) - The one and only + +- [Anson Biggs](https://blog.ansonbiggs.com/author/anson/) - The one and only ## 🎉 Acknowledgements -+ Discord for having a great bot API -+ IEX Cloud for offering a free tier -+ Viewers like you ♥ + +- Discord for having a great bot API +- IEX Cloud for offering a free tier +- Viewers like you ♥ -- 2.51.2 From d456d68167d09e36457f2623bd1dc9818a8c08a6 Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 12 Jun 2020 01:55:42 -0700 Subject: [PATCH 35/75] removed help info for inline command --- simple-stock-bots/simple-discord-stock-bot/functions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 30cf834e1..7c5d3cae6 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -28,9 +28,6 @@ Full documentation can be found [here.](https://simple-stock-bots.gitlab.io/site - /info `$[symbol]` will return general information about the symbol. - /search `query` Takes a search string, whether a company name or ticker and returns a list of companies that are supported by the bot. -**Inline Features** - You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. - The bot also looks at every message in any chat it is in for stock symbols. Symbols start with a `$` followed by the stock symbol. For example: $tsla would return price information for Tesla Motors. `Market data is provided by [IEX Cloud](https://iexcloud.io)` -- 2.51.2 From 9082dbdca808960191d355eec27fc0e65fec92ac Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 12 Jun 2020 01:56:09 -0700 Subject: [PATCH 36/75] small typo in help text --- simple-stock-bots/simple-discord-stock-bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index aefd2c079..ddd307f67 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -58,7 +58,7 @@ async def on_message(message): else: await message.channel.send( - "Command requires a ticker. See /help for more information." + "Command requires a query. See /help for more information." ) elif message.content.startswith("/help"): -- 2.51.2 From 7b20114c217f73f8dfad70df171fbd170dbb5949 Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 12 Jun 2020 02:51:46 -0700 Subject: [PATCH 37/75] help link was incorrect --- simple-stock-bots/simple-discord-stock-bot/bot.py | 1 - simple-stock-bots/simple-discord-stock-bot/functions.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index ddd307f67..c28cd6ede 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -63,7 +63,6 @@ async def on_message(message): elif message.content.startswith("/help"): """Send link to docs when the command /help is issued.""" - reply = "[Please see the documentation for Bot information](https://simple-stock-bots.gitlab.io/site/discord/)" await message.channel.send(s.help_text) # If no commands, check for any tickers. diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 7c5d3cae6..23a1ba42c 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -20,7 +20,7 @@ class Symbol: help_text = """ Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) -Full documentation can be found [here.](https://simple-stock-bots.gitlab.io/site/telegram/) +Full documentation can be found [here.](https://simple-stock-bots.gitlab.io/site/) **Commands** - /dividend `$[symbol]` will return dividend information for the symbol. -- 2.51.2 From 6462e6dffafae06abab7ed6b39899dea588c5943 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 24 Jan 2021 12:46:42 -0700 Subject: [PATCH 38/75] bot was out of date again. --- simple-stock-bots/simple-discord-stock-bot/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 53663460b..3193ef06c 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,4 +1,4 @@ -discord.py==1.3.3 +discord.py==1.6 requests==2.23.0 pandas==1.0.3 fuzzywuzzy==0.18.0 -- 2.51.2 From 2b89b735bad48b20ebf4d5092cdb04f74abf4c99 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 24 Jan 2021 14:56:16 -0700 Subject: [PATCH 39/75] fixed vulnerability --- simple-stock-bots/simple-discord-stock-bot/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 3193ef06c..31294566a 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -2,5 +2,5 @@ discord.py==1.6 requests==2.23.0 pandas==1.0.3 fuzzywuzzy==0.18.0 -python-Levenshtein==0.12.0 +python-Levenshtein==0.12.1 schedule==0.6.0 \ No newline at end of file -- 2.51.2 From b627a6956eb7d9384fe391c83b3fb5743d1b4d16 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 16:56:42 -0700 Subject: [PATCH 40/75] Close #17 and worked on #3 --- .../simple-discord-stock-bot/bot.py | 136 +++-- .../simple-discord-stock-bot/functions.py | 522 ++++++++++++++---- .../simple-discord-stock-bot/requirements.txt | 7 +- 3 files changed, 509 insertions(+), 156 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index c28cd6ede..60f294178 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,78 +1,94 @@ +import os + import discord +from discord.ext import commands + from functions import Symbol -import os client = discord.Client() -s = Symbol(os.environ["IEX"]) +DISCORD_TOKEN = os.environ["DISCORD"] + +try: + IEX_TOKEN = os.environ["IEX"] +except KeyError: + IEX_TOKEN = "" + print("Starting without an IEX Token will not allow you to get market data!") + + +intents = discord.Intents.default() +intents.members = True +bot = commands.Bot( + command_prefix="/", + description="Simple bot for getting stock market information.", + intents=intents, +) -@client.event + +@bot.event async def on_ready(): - print("We have logged in as {0.user}".format(client)) + print("Starting Simple Stock Bot") + print("Logged in as") + print(bot.user.name) + print(bot.user.id) + print("------") -@client.event +@bot.event async def on_message(message): + if message.author == client.user: return - # Check for dividend command - if message.content.startswith("/dividend"): - replies = s.dividend_reply(s.find_symbols(message.content)) - if replies: - for reply in replies.items(): - await message.channel.send(reply[1]) - else: + if "$" in message.content: + symbols = s.find_symbols(message.content) - await message.channel.send( - "Command requires a ticker. See /help for more information." - ) + if symbols: - elif message.content.startswith("/news"): - replies = s.news_reply(s.find_symbols(message.content)) - if replies: - for reply in replies.items(): - await message.channel.send(reply[1]) - else: - await message.channel.send( - "Command requires a ticker. See /help for more information." - ) - - elif message.content.startswith("/info"): - replies = s.info_reply(s.find_symbols(message.content)) - if replies: - for reply in replies.items(): + for reply in s.price_reply(symbols).items(): await message.channel.send(reply[1]) - else: - await message.channel.send( - "Command requires a ticker. See /help for more information." - ) - - elif message.content.startswith("/search"): - queries = s.search_symbols(message.content[7:])[:6] - if queries: - reply = "*Search Results:*\n`$ticker: Company Name`\n" - for query in queries: - reply += "`" + query[1] + "`\n" - await message.channel.send(reply) - - else: - await message.channel.send( - "Command requires a query. See /help for more information." - ) - - elif message.content.startswith("/help"): - """Send link to docs when the command /help is issued.""" - await message.channel.send(s.help_text) - - # If no commands, check for any tickers. - else: - replies = s.price_reply(s.find_symbols(message.content)) - if replies: - for reply in replies.items(): - await message.channel.send(reply[1]) - else: - return -client.run(os.environ["DISCORD"]) +@bot.command(description="Information on how to donate.") +async def donate(ctx, cmd: str): + print("donate") + await ctx.send("donate:" + cmd) + + +@bot.command() +async def dividend(ctx, cmd: str): + await ctx.send("dividend:" + cmd) + + +@bot.command() +async def intra(ctx, cmd: str): + await ctx.send("intra:" + cmd) + + +@bot.command() +async def chart(ctx, cmd: str): + await ctx.send("chart:" + cmd) + + +@bot.command() +async def news(ctx, cmd: str): + await ctx.send("news:" + cmd) + + +@bot.command() +async def info(ctx, cmd: str): + await ctx.send("info:" + cmd) + + +@bot.command() +async def stat(ctx, cmd: str): + await ctx.send("stat:" + cmd) + + +# @bot.command() +# async def help(ctx, cmd: str): +# await ctx.send("help:" + cmd) + + +s = Symbol(IEX_TOKEN) +bot.run(DISCORD_TOKEN) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 23a1ba42c..a65a30823 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -1,11 +1,11 @@ -import json import re -from datetime import datetime, timedelta +from datetime import datetime +from typing import Optional, List, Tuple, Dict import pandas as pd import requests as r -from fuzzywuzzy import fuzz import schedule +from fuzzywuzzy import fuzz class Symbol: @@ -16,36 +16,79 @@ class Symbol: SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" searched_symbols = {} + charts = {} + + license = re.sub( + r"\b\n", + " ", + r.get( + "https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE" + ).text, + ) help_text = """ Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) -Full documentation can be found [here.](https://simple-stock-bots.gitlab.io/site/) +Keep up with the latest news for the bot in itsTelegram Channel: https://t.me/simplestockbotnews + +Full documentation on using and running your own stock bot can be found [here.](https://simple-stock-bots.gitlab.io/site) **Commands** - - /dividend `$[symbol]` will return dividend information for the symbol. - - /news `$[symbol]` will return news about the symbol. - - /info `$[symbol]` will return general information about the symbol. - - /search `query` Takes a search string, whether a company name or ticker and returns a list of companies that are supported by the bot. + - /donate [amount in USD] to donate. 🎗️ + - /dividend $[symbol] will return dividend information for the symbol. 📅 + - /intra $[symbol] Plot of the stocks movement since the last market open. 📈 + - /chart $[symbol] Plot of the stocks movement for the past 1 month. 📊 + - /news $[symbol] News about the symbol. 📰 + - /info $[symbol] General information about the symbol. ℹ️ + - /stat $[symbol] Key statistics about the symbol. 🔢 + - /help Get some help using the bot. 🆘 + +**Inline Features** + You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots + full list of stock symbols and return the price of the ticker. Then once you select the ticker + want the bot will send a message as you in that chat with the latest stock price. + The bot also looks at every message in any chat it is in for stock symbols.Symbols start with a + `$` followed by the stock symbol. For example:$tsla would return price information for Tesla Motors. + Market data is provided by [IEX Cloud](https://iexcloud.io) + + If you believe the bot is not behaving properly run `/status`. + """ + + donate_text = """ +Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson) +All donations go directly towards paying for servers, and market data is provided by +[IEX Cloud](https://iexcloud.io/). + +The easiest way to donate is to run the `/donate [amount in USD]` command with USdollars you would like to donate. -The bot also looks at every message in any chat it is in for stock symbols. Symbols start with a `$` followed by the stock symbol. For example: $tsla would return price information for Tesla Motors. +Example: `/donate 2` would donate 2 USD. +An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card. +If you have any questions get in touch: @MisterBiggs or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/) -`Market data is provided by [IEX Cloud](https://iexcloud.io)` +_Donations can only be made in a chat directly with @simplestockbot_ """ - def __init__(self, IEX_TOKEN: str): - self.IEX_TOKEN = IEX_TOKEN - self.get_symbol_list() - schedule.every().day.do(self.get_symbol_list) + def __init__(self, IEX_TOKEN: str) -> None: + """Creates a Symbol Object - def get_symbol_list(self, return_df=False): - """ - Fetches a list of stock market symbols from FINRA - - Returns: - pd.DataFrame -- [DataFrame with columns: Symbol | Issue_Name | Primary_Listing_Mkt - datetime -- The time when the list of symbols was fetched. The Symbol list is updated every open and close of every trading day. + Parameters + ---------- + IEX_TOKEN : str + IEX Token """ + self.IEX_TOKEN = IEX_TOKEN + if IEX_TOKEN != "": + self.get_symbol_list() + + schedule.every().day.do(self.get_symbol_list) + schedule.every().day.do(self.clear_charts) + + def clear_charts(self) -> None: + """Clears cache of chart data.""" + self.charts = {} + + def get_symbol_list(self, return_df=False) -> Optional[pd.DataFrame]: + raw_symbols = r.get( f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" ).json() @@ -56,16 +99,62 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb if return_df: return symbols, datetime.now() - def search_symbols(self, search: str): + def iex_status(self) -> str: + """Checks IEX Status dashboard for any current API issues. + + Returns + ------- + str + Human readable text on status of IEX API + """ + status = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json").json()[ + "status" + ] + + if status["indicator"] == "none": + return "IEX Cloud is currently not reporting any issues with its API." + else: + return ( + f"{status['indicator']}: {status['description']}." + + " Please check the status page for more information. https://status.iexapis.com" + ) + + def message_status(self) -> str: + """Checks to see if the bot has available IEX Credits + + Returns + ------- + str + Human readable text on status of IEX Credits. """ - Performs a fuzzy search to find stock symbols closest to a search term. - - Arguments: - search {str} -- String used to search, could be a company name or something close to the companies stock ticker. - - Returns: - List of Tuples -- A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + usage = r.get( + f"https://cloud.iexapis.com/stable/account/metadata?token={self.IEX_TOKEN}" + ).json() + try: + if ( + usage["messagesUsed"] >= usage["messageLimit"] - 10000 + and not usage["payAsYouGoEnabled"] + ): + return "Bot may be out of IEX Credits." + else: + return "Bot has available IEX Credits." + except KeyError: + return "**IEX API could not be reached.**" + + def search_symbols(self, search: str) -> List[Tuple[str, str]]: + """Performs a fuzzy search to find stock symbols closest to a search term. + + Parameters + ---------- + search : str + String used to search, could be a company name or something close to the companies stock ticker. + + Returns + ------- + List[tuple[str, str]] + A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). """ + schedule.run_pending() search = search.lower() try: # https://stackoverflow.com/a/3845776/8774114 @@ -75,13 +164,15 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb symbols = self.symbol_list symbols["Match"] = symbols.apply( - lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), axis=1, + lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), + axis=1, ) symbols.sort_values(by="Match", ascending=False, inplace=True) if symbols["Match"].head().sum() < 300: symbols["Match"] = symbols.apply( - lambda x: fuzz.partial_ratio(search, x["name"].lower()), axis=1, + lambda x: fuzz.partial_ratio(search, x["name"].lower()), + axis=1, ) symbols.sort_values(by="Match", ascending=False, inplace=True) @@ -90,28 +181,36 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb self.searched_symbols[search] = symbol_list return symbol_list - def find_symbols(self, text: str): - """ - Finds stock tickers starting with a dollar sign in a blob of text and returns them in a list. Only returns each match once. Example: Whats the price of $tsla? -> ['tsla'] - - Arguments: - text {str} -- Blob of text that might contain tickers with the format: $TICKER - - Returns: - list -- List of every found match without the dollar sign. + def find_symbols(self, text: str) -> List[str]: + """Finds stock tickers starting with a dollar sign in a blob of text and returns them in a list. + Only returns each match once. Example: Whats the price of $tsla? + + Parameters + ---------- + text : str + Blob of text. + + Returns + ------- + List[str] + List of stock symbols as strings without dollar sign. """ return list(set(re.findall(self.SYMBOL_REGEX, text))) - def price_reply(self, symbols: list): - """ - Takes a list of symbols and replies with Markdown formatted text about the symbols price change for the day. - - Arguments: - symbols {list} -- List of stock market symbols. - - Returns: - dict -- Dictionary with keys of symbols and values of markdown formatted text example: {'tsla': 'The current stock price of Tesla Motors is $**420$$, the stock price is currently **up 42%**} + def price_reply(self, symbols: list) -> Dict[str, str]: + """Returns current market price or after hours if its available for a given stock symbol. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + markdown formatted string of the symbols price and movement. """ dataMessages = {} for symbol in symbols: @@ -120,15 +219,46 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb response = r.get(IEXurl) if response.status_code == 200: IEXData = response.json() - message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" - # Determine wording of change text - change = round(IEXData["changePercent"] * 100, 2) - if change > 0: - message += f", the stock is currently **up {change}%**" - elif change < 0: - message += f", the stock is currently **down {change}%**" + keys = ( + "isUSMarketOpen", + "extendedChangePercent", + "extendedPrice", + "companyName", + "latestPrice", + "changePercent", + ) + + if set(keys).issubset(IEXData): + + try: # Some symbols dont return if the market is open + IEXData["isUSMarketOpen"] + except KeyError: + IEXData["isUSMarketOpen"] = True + + if ( + IEXData["isUSMarketOpen"] + or (IEXData["extendedChangePercent"] is None) + or (IEXData["extendedPrice"] is None) + ): # Check if market is open. + message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" + change = round(IEXData["changePercent"] * 100, 2) + else: + message = ( + f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}**," + + f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**" + ) + change = round(IEXData["extendedChangePercent"] * 100, 2) + + # Determine wording of change text + if change > 0: + message += f", the stock is currently **up {change}%**" + elif change < 0: + message += f", the stock is currently **down {change}%**" + else: + message += ", the stock hasn't shown any movement today." else: - message += ", the stock hasn't shown any movement today." + message = f"The symbol: {symbol} encountered and error. This could be due to " + else: message = f"The symbol: {symbol} was not found." @@ -136,52 +266,95 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb return dataMessages - def dividend_reply(self, symbols: list): - divMessages = {} + def dividend_reply(self, symbol: str) -> Dict[str, str]: + """Returns the most recent, or next dividend date for a stock symbol. - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/data-points/{symbol}/NEXTDIVIDENDDATE?token={self.IEX_TOKEN}" - response = r.get(IEXurl) - if response.status_code == 200: + Parameters + ---------- + symbols : list + List of stock symbols. - # extract date from json - date = response.json() - # Pattern IEX uses for dividend date. - pattern = "%Y-%m-%d" - divDate = datetime.strptime(date, pattern) + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted string of the symbols div dates. + """ - daysDelta = (divDate - datetime.now()).days - datePretty = divDate.strftime("%A, %B %w") - if daysDelta < 0: - divMessages[ - symbol - ] = f"{symbol.upper()} dividend was on {datePretty} and a new date hasn't been announced yet." - elif daysDelta > 0: - divMessages[ - symbol - ] = f"{symbol.upper()} dividend is on {datePretty} which is in {daysDelta} Days." - else: - divMessages[symbol] = f"{symbol.upper()} is today." + IEXurl = f"https://cloud.iexapis.com/stable/stock/msft/dividends/next?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200: + IEXData = response.json()[0] + keys = ( + "amount", + "currency", + "declaredDate", + "exDate", + "frequency", + "paymentDate", + "flag", + ) - else: - divMessages[ - symbol - ] = f"{symbol} either doesn't exist or pays no dividend." + if set(keys).issubset(IEXData): + + if IEXData["currency"] == "USD": + price = f"${IEXData['amount']}" + else: + price = f"{IEXData['amount']} {IEXData['currency']}" - return divMessages + # Pattern IEX uses for dividend date. + pattern = "%Y-%m-%d" - def news_reply(self, symbols: list): + declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime( + "%A, %B %w" + ) + ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w") + payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime( + "%A, %B %w" + ) + + daysDelta = ( + datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now() + ).days + + return ( + f"The next dividend for ${self.symbol_list[self.symbol_list['symbol']==symbol.upper()]['description'].item()}" + + f" is on {payment} which is in {daysDelta} days." + + f" The dividend is for {price} per share." + + f"\nThe dividend was declared on {declared} and the ex-dividend date is {ex}" + ) + + return f"{symbol} either doesn't exist or pays no dividend." + + def news_reply(self, symbols: list) -> Dict[str, str]: + """Gets recent english news on stock symbols. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable markdown formatted string of the symbols news. + """ newsMessages = {} for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/3?token={self.IEX_TOKEN}" + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/5?token={self.IEX_TOKEN}" response = r.get(IEXurl) if response.status_code == 200: data = response.json() - newsMessages[symbol] = f"News for **{symbol.upper()}**:\n" - for news in data: - message = f"\t[{news['headline']}]({news['url']})\n\n" - newsMessages[symbol] = newsMessages[symbol] + message + if len(data): + newsMessages[symbol] = f"News for **{symbol.upper()}**:\n\n" + for news in data: + if news["lang"] == "en" and not news["hasPaywall"]: + message = f"*{news['source']}*: [{news['headline']}]({news['url']})\n" + newsMessages[symbol] = newsMessages[symbol] + message + else: + newsMessages[ + symbol + ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." else: newsMessages[ symbol @@ -189,7 +362,19 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb return newsMessages - def info_reply(self, symbols: list): + def info_reply(self, symbols: List[str]) -> Dict[str, str]: + """Gets information on stock symbols. + + Parameters + ---------- + symbols : List[str] + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted string of the symbols information. + """ infoMessages = {} for symbol in symbols: @@ -198,13 +383,164 @@ The bot also looks at every message in any chat it is in for stock symbols. Symb if response.status_code == 200: data = response.json() + infoMessages[symbol] = ( + f"Company Name: [{data['companyName']}]({data['website']})\nIndustry:" + + f" {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" + ) + + else: infoMessages[ symbol - ] = f"Company Name: [{data['companyName']}]({data['website']})\nIndustry: {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" + ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + + return infoMessages + + def intra_reply(self, symbol: str) -> pd.DataFrame: + """Returns price data for a symbol since the last market open. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + if symbol.upper() not in list(self.symbol_list["symbol"]): + return pd.DataFrame() + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200: + df = pd.DataFrame(response.json()) + df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) + df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) + df = df.set_index("DT") + return df + + return pd.DataFrame() + + def chart_reply(self, symbol: str) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + schedule.run_pending() + + if symbol.upper() not in list(self.symbol_list["symbol"]): + return pd.DataFrame() + + try: # https://stackoverflow.com/a/3845776/8774114 + return self.charts[symbol.upper()] + except KeyError: + pass + + response = r.get( + f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false" + ) + if response.status_code == 200: + df = pd.DataFrame(response.json()) + df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) + df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) + df = df.set_index("DT") + self.charts[symbol.upper()] = df + return df + + return pd.DataFrame() + + def stat_reply(self, symbols: List[str]) -> Dict[str, str]: + """Gets key statistics for each symbol in the list + + Parameters + ---------- + symbols : List[str] + List of stock symbols + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics. + """ + infoMessages = {} + + for symbol in symbols: + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + + if response.status_code == 200: + data = response.json() + [data.pop(k) for k in list(data) if data[k] == ""] + + m = "" + if "companyName" in data: + m += f"Company Name: {data['companyName']}\n" + if "marketcap" in data: + m += f"Market Cap: {data['marketcap']:,}\n" + if "week52high" in data: + m += f"52 Week (high-low): {data['week52high']:,} " + if "week52low" in data: + m += f"- {data['week52low']:,}\n" + if "employees" in data: + m += f"Number of Employees: {data['employees']:,}\n" + if "nextEarningsDate" in data: + m += f"Next Earnings Date: {data['nextEarningsDate']}\n" + if "peRatio" in data: + m += f"Price to Earnings: {data['peRatio']:.3f}\n" + if "beta" in data: + m += f"Beta: {data['beta']:.3f}\n" + infoMessages[symbol] = m else: infoMessages[ symbol ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." return infoMessages + + def crypto_reply(self, pair: str) -> str: + """Returns the current price of a cryptocurrency + + Parameters + ---------- + pair : str + symbol for the cryptocurrency, sometimes with a price pair like ETHUSD + + Returns + ------- + str + Returns a human readable markdown description of the price, or an empty string if no price was found. + """ + + pair = pair.split(" ")[-1].replace("/", "").upper() + pair += "USD" if len(pair) == 3 else pair + + IEXurl = f"https://cloud.iexapis.com/stable/crypto/{pair}/quote?token={self.IEX_TOKEN}" + + response = r.get(IEXurl) + + if response.status_code == 200: + data = response.json() + + quote = f"Symbol: {data['symbol']}\n" + quote += f"Price: ${data['latestPrice']}\n" + + new, old = data["latestPrice"], data["previousClose"] + if old is not None: + change = (float(new) - float(old)) / float(old) + quote += f"Change: {change}\n" + + return quote + + else: + return "" diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 31294566a..021548087 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,6 +1,7 @@ discord.py==1.6 -requests==2.23.0 -pandas==1.0.3 +requests==2.25.1 +pandas==1.2.1 fuzzywuzzy==0.18.0 python-Levenshtein==0.12.1 -schedule==0.6.0 \ No newline at end of file +schedule==1.0.0 +mplfinance==0.12.7a5 \ No newline at end of file -- 2.51.2 From 61e92593d92059f5d1ea07f1f9938f670bd8af36 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:01:55 -0700 Subject: [PATCH 41/75] formating and adding rest of commands. --- .../simple-discord-stock-bot/bot.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 60f294178..8028e5cc3 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -55,39 +55,44 @@ async def donate(ctx, cmd: str): await ctx.send("donate:" + cmd) +@bot.command() +async def stat(ctx, cmd: str): + await ctx.send("stat:" + cmd) + + @bot.command() async def dividend(ctx, cmd: str): await ctx.send("dividend:" + cmd) @bot.command() -async def intra(ctx, cmd: str): - await ctx.send("intra:" + cmd) +async def news(ctx, cmd: str): + await ctx.send("news:" + cmd) @bot.command() -async def chart(ctx, cmd: str): - await ctx.send("chart:" + cmd) +async def info(ctx, cmd: str): + await ctx.send("info:" + cmd) @bot.command() -async def news(ctx, cmd: str): - await ctx.send("news:" + cmd) +async def search(ctx, cmd: str): + await ctx.send("search:" + cmd) @bot.command() -async def info(ctx, cmd: str): - await ctx.send("info:" + cmd) +async def intra(ctx, cmd: str): + await ctx.send("intra:" + cmd) @bot.command() -async def stat(ctx, cmd: str): - await ctx.send("stat:" + cmd) +async def chart(ctx, cmd: str): + await ctx.send("chart:" + cmd) -# @bot.command() -# async def help(ctx, cmd: str): -# await ctx.send("help:" + cmd) +@bot.command() +async def crypto(ctx, cmd: str): + await ctx.send("crypto" + cmd) s = Symbol(IEX_TOKEN) -- 2.51.2 From 087b2d8bf01af88903f43a2a94b7305bd1513465 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:02:12 -0700 Subject: [PATCH 42/75] Close #7 --- .../simple-discord-stock-bot/bot.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 8028e5cc3..7627f5fc8 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -91,8 +91,28 @@ async def chart(ctx, cmd: str): @bot.command() -async def crypto(ctx, cmd: str): - await ctx.send("crypto" + cmd) +async def crypto(ctx, symbol: str): + reply = s.crypto_reply(symbol) + await ctx.send(reply) + + +@bot.event +async def on_message(message): + + if message.author == client.user: + return + + if message.content[0] == "/": + await bot.process_commands(message) + return + + if "$" in message.content: + symbols = s.find_symbols(message.content) + + if symbols: + for reply in s.price_reply(symbols).items(): + await message.channel.send(reply[1]) + return s = Symbol(IEX_TOKEN) -- 2.51.2 From 57760951a27127d030c4d246f93d8d2c5a42ec50 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:05:08 -0700 Subject: [PATCH 43/75] Close #10 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 7627f5fc8..9e0df7037 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -76,8 +76,13 @@ async def info(ctx, cmd: str): @bot.command() -async def search(ctx, cmd: str): - await ctx.send("search:" + cmd) +async def search(ctx, query: str): + results = s.search_symbols(query) + if results: + reply = "*Search Results:*\n`$ticker: Company Name`\n" + for query in results: + reply += "`" + query[1] + "`\n" + await ctx.send(reply) @bot.command() -- 2.51.2 From a23dc5c6b3bf2ebc363d432035d1b5e537b33664 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:09:46 -0700 Subject: [PATCH 44/75] Close #12 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 9e0df7037..4ce53605b 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -66,13 +66,12 @@ async def dividend(ctx, cmd: str): @bot.command() -async def news(ctx, cmd: str): - await ctx.send("news:" + cmd) +async def info(ctx, sym: str): + symbols = s.find_symbols(sym) - -@bot.command() -async def info(ctx, cmd: str): - await ctx.send("info:" + cmd) + if symbols: + for reply in s.info_reply(symbols).items(): + await ctx.send(reply[1]) @bot.command() -- 2.51.2 From 7b334068e6e4e88967120e570948468fb848fb02 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:10:35 -0700 Subject: [PATCH 45/75] Close #13 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 4ce53605b..ff7af52d6 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -56,13 +56,12 @@ async def donate(ctx, cmd: str): @bot.command() -async def stat(ctx, cmd: str): - await ctx.send("stat:" + cmd) - +async def news(ctx, sym: str): + symbols = s.find_symbols(sym) -@bot.command() -async def dividend(ctx, cmd: str): - await ctx.send("dividend:" + cmd) + if symbols: + for reply in s.news_reply(symbols).items(): + await ctx.send(reply[1]) @bot.command() -- 2.51.2 From 4f27eac0f9ddfc8dea79f1f78e96e4ca75877575 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:13:15 -0700 Subject: [PATCH 46/75] Close #14 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index ff7af52d6..5e82a0c86 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -48,11 +48,13 @@ async def on_message(message): for reply in s.price_reply(symbols).items(): await message.channel.send(reply[1]) +@bot.command() +async def dividend(ctx, sym: str): + symbols = s.find_symbols(sym) -@bot.command(description="Information on how to donate.") -async def donate(ctx, cmd: str): - print("donate") - await ctx.send("donate:" + cmd) + if symbols: + for symbol in symbols: + await ctx.send(s.dividend_reply(symbol)) @bot.command() -- 2.51.2 From c4e50f565bc8a6920058da10be1650bc82855ad5 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:13:44 -0700 Subject: [PATCH 47/75] Close #11 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 5e82a0c86..c5d62f4b6 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -40,10 +40,13 @@ async def on_message(message): if message.author == client.user: return - if "$" in message.content: - symbols = s.find_symbols(message.content) +@bot.command() +async def stat(ctx, sym: str): + symbols = s.find_symbols(sym) - if symbols: + if symbols: + for reply in s.stat_reply(symbols).items(): + await ctx.send(reply[1]) for reply in s.price_reply(symbols).items(): await message.channel.send(reply[1]) -- 2.51.2 From c674781f2b490692aa7775d516cb418d6c1fcf8b Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:14:20 -0700 Subject: [PATCH 48/75] Close #5 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index c5d62f4b6..fb4a0682f 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -34,8 +34,9 @@ async def on_ready(): print("------") -@bot.event -async def on_message(message): +@bot.command() +async def donate(ctx): + await ctx.send(s.donate_text) if message.author == client.user: return -- 2.51.2 From d60b978d97e700e3f6e13f198321f3452c87ef4c Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:16:16 -0700 Subject: [PATCH 49/75] close #15 --- simple-stock-bots/simple-discord-stock-bot/bot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index fb4a0682f..46743946b 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -34,6 +34,11 @@ async def on_ready(): print("------") +@bot.command() +async def license(ctx): + await ctx.send(s.license) + + @bot.command() async def donate(ctx): await ctx.send(s.donate_text) -- 2.51.2 From cfd86e9e217793f1e6d3e55e511485eb07c7017f Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 18:25:09 -0700 Subject: [PATCH 50/75] #4 --- .../simple-discord-stock-bot/bot.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 46743946b..7417ab8cb 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -34,6 +34,23 @@ async def on_ready(): print("------") +@bot.command() +async def status(ctx): + message = "" + try: + # IEX Status + message += s.iex_status() + "\n" + + # Message Status + message += s.message_status() + except Exception as ex: + message += ( + f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n" + + "*The bot encountered an error while attempting to find errors. Please contact the bot admin.*" + ) + await ctx.send(message) + + @bot.command() async def license(ctx): await ctx.send(s.license) -- 2.51.2 From 3cbb5fbf305e46ce68c9c55d3f2ee28a9525560c Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 19:32:58 -0700 Subject: [PATCH 51/75] Close #9 --- .../simple-discord-stock-bot/bot.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 7417ab8cb..c1b016c1d 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -127,6 +127,49 @@ async def crypto(ctx, symbol: str): await ctx.send(reply) +import pandas as pd +import io + +import mplfinance as mpf + + +@bot.command() +async def intra(ctx, sym: str): + + symbol = s.find_symbols(sym)[0] + + df = s.intra_reply(symbol) + if df.empty: + await ctx.send("Invalid symbol please see `/help` for usage details.") + return + + buf = io.BytesIO() + mpf.plot( + df, + type="renko", + title=f"\n${symbol.upper()}", + volume=True, + style="yahoo", + mav=20, + savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), + ) + buf.seek(0) + + caption = ( + f"\nIntraday chart for ${symbol.upper()} from {df.first_valid_index().strftime('%I:%M')} to" + + f" {df.last_valid_index().strftime('%I:%M')} ET on" + + f" {datetime.date.today().strftime('%d, %b %Y')}" + ) + + await ctx.send( + content=caption, + file=discord.File( + buf, + filename=f"{symbol.upper()}:{datetime.date.today().strftime('%d%b%Y')}.png", + ), + ) + + @bot.event async def on_message(message): -- 2.51.2 From 0720d0b80329d469a13c2e68374755060fabbb59 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 19:38:19 -0700 Subject: [PATCH 52/75] Close #8 --- .../simple-discord-stock-bot/bot.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index c1b016c1d..d2854fd72 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -170,6 +170,41 @@ async def intra(ctx, sym: str): ) +@bot.command() +async def chart(ctx, sym: str): + + symbol = s.find_symbols(sym)[0] + + df = s.intra_reply(symbol) + if df.empty: + await ctx.send("Invalid symbol please see `/help` for usage details.") + return + + buf = io.BytesIO() + mpf.plot( + df, + type="candle", + title=f"\n${symbol.upper()}", + volume=True, + style="yahoo", + savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), + ) + buf.seek(0) + + caption = ( + f"\n1 Month chart for ${symbol.upper()} from {df.first_valid_index().strftime('%d, %b %Y')}" + + f" to {df.last_valid_index().strftime('%d, %b %Y')}" + ) + + await ctx.send( + content=caption, + file=discord.File( + buf, + filename=f"{symbol.upper()}:{datetime.date.today().strftime('1M%d%b%Y')}.png", + ), + ) + + @bot.event async def on_message(message): -- 2.51.2 From 499228b3fbd4f294d193d4122cabeab7bf19fb2c Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 19:40:58 -0700 Subject: [PATCH 53/75] Close #3 --- .../simple-discord-stock-bot/bot.py | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index d2854fd72..535b93946 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,12 +1,14 @@ +import datetime +import io import os import discord +import mplfinance as mpf +import pandas as pd from discord.ext import commands from functions import Symbol -client = discord.Client() - DISCORD_TOKEN = os.environ["DISCORD"] try: @@ -16,6 +18,8 @@ except KeyError: print("Starting without an IEX Token will not allow you to get market data!") +client = discord.Client() + intents = discord.Intents.default() intents.members = True bot = commands.Bot( @@ -60,22 +64,18 @@ async def license(ctx): async def donate(ctx): await ctx.send(s.donate_text) - if message.author == client.user: - return @bot.command() -async def stat(ctx, sym: str): +async def stat(ctx, *, sym: str): symbols = s.find_symbols(sym) if symbols: for reply in s.stat_reply(symbols).items(): await ctx.send(reply[1]) - for reply in s.price_reply(symbols).items(): - await message.channel.send(reply[1]) @bot.command() -async def dividend(ctx, sym: str): +async def dividend(ctx, *, sym: str): symbols = s.find_symbols(sym) if symbols: @@ -84,7 +84,7 @@ async def dividend(ctx, sym: str): @bot.command() -async def news(ctx, sym: str): +async def news(ctx, *, sym: str): symbols = s.find_symbols(sym) if symbols: @@ -93,7 +93,7 @@ async def news(ctx, sym: str): @bot.command() -async def info(ctx, sym: str): +async def info(ctx, *, sym: str): symbols = s.find_symbols(sym) if symbols: @@ -102,7 +102,7 @@ async def info(ctx, sym: str): @bot.command() -async def search(ctx, query: str): +async def search(ctx, *, query: str): results = s.search_symbols(query) if results: reply = "*Search Results:*\n`$ticker: Company Name`\n" @@ -111,28 +111,12 @@ async def search(ctx, query: str): await ctx.send(reply) -@bot.command() -async def intra(ctx, cmd: str): - await ctx.send("intra:" + cmd) - - -@bot.command() -async def chart(ctx, cmd: str): - await ctx.send("chart:" + cmd) - - @bot.command() async def crypto(ctx, symbol: str): reply = s.crypto_reply(symbol) await ctx.send(reply) -import pandas as pd -import io - -import mplfinance as mpf - - @bot.command() async def intra(ctx, sym: str): -- 2.51.2 From f60af8ad03ee0d1d19508ff9e44025ef610e4344 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 20:49:23 -0700 Subject: [PATCH 54/75] documentation --- .../simple-discord-stock-bot/bot.py | 22 +++++++++++++++---- .../simple-discord-stock-bot/functions.py | 4 ++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 535b93946..f6cf617b8 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -2,14 +2,14 @@ import datetime import io import os -import discord import mplfinance as mpf -import pandas as pd + +import discord from discord.ext import commands from functions import Symbol -DISCORD_TOKEN = os.environ["DISCORD"] +DISCORD_TOKEN = "NjYyNzcyOTU5MzMyNTMyMjg2.Xg-1aw.Au28SCJJE3HLy23b3adREtLXFiY" try: IEX_TOKEN = os.environ["IEX"] @@ -24,7 +24,9 @@ intents = discord.Intents.default() intents.members = True bot = commands.Bot( command_prefix="/", - description="Simple bot for getting stock market information.", + description="Simple bot for getting stock market information. " + + "The bot responds to any ticker in a message with a dollar sign: $tsla. " + + "It also supports the commands below.", intents=intents, ) @@ -40,8 +42,10 @@ async def on_ready(): @bot.command() async def status(ctx): + """Debug command for diagnosing if the bot is experiencing any issues.""" message = "" try: + message = "Contact MisterBiggs#0465 if you need help.\n" # IEX Status message += s.iex_status() + "\n" @@ -57,16 +61,19 @@ async def status(ctx): @bot.command() async def license(ctx): + """Returns the bots license agreement.""" await ctx.send(s.license) @bot.command() async def donate(ctx): + """Details on how to support the development and hosting of the bot.""" await ctx.send(s.donate_text) @bot.command() async def stat(ctx, *, sym: str): + """Get statistics on a list of stock symbols.""" symbols = s.find_symbols(sym) if symbols: @@ -76,6 +83,7 @@ async def stat(ctx, *, sym: str): @bot.command() async def dividend(ctx, *, sym: str): + """Get dividend information on a stock symbol.""" symbols = s.find_symbols(sym) if symbols: @@ -85,6 +93,7 @@ async def dividend(ctx, *, sym: str): @bot.command() async def news(ctx, *, sym: str): + """Get recent english news on a stock symbol.""" symbols = s.find_symbols(sym) if symbols: @@ -94,6 +103,7 @@ async def news(ctx, *, sym: str): @bot.command() async def info(ctx, *, sym: str): + """Get information of a stock ticker.""" symbols = s.find_symbols(sym) if symbols: @@ -103,6 +113,7 @@ async def info(ctx, *, sym: str): @bot.command() async def search(ctx, *, query: str): + """Search for a stock symbol using either symbol of company name.""" results = s.search_symbols(query) if results: reply = "*Search Results:*\n`$ticker: Company Name`\n" @@ -113,12 +124,14 @@ async def search(ctx, *, query: str): @bot.command() async def crypto(ctx, symbol: str): + """Get the price of a cryptocurrency using in USD.""" reply = s.crypto_reply(symbol) await ctx.send(reply) @bot.command() async def intra(ctx, sym: str): + """Get a chart for the stocks movement since market open.""" symbol = s.find_symbols(sym)[0] @@ -156,6 +169,7 @@ async def intra(ctx, sym: str): @bot.command() async def chart(ctx, sym: str): + """Get a chart for the stocks movement for the past month.""" symbol = s.find_symbols(sym)[0] diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index a65a30823..4f04fd7da 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -55,7 +55,7 @@ Full documentation on using and running your own stock bot can be found [here.]( """ donate_text = """ -Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson) +Simple Stock Bot is run entirely on donations. All donations go directly towards paying for servers, and market data is provided by [IEX Cloud](https://iexcloud.io/). @@ -63,7 +63,7 @@ The easiest way to donate is to run the `/donate [amount in USD]` command with U Example: `/donate 2` would donate 2 USD. An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card. -If you have any questions get in touch: @MisterBiggs or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/) +If you have any questions get in touch: MisterBiggs#0465 or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/) _Donations can only be made in a chat directly with @simplestockbot_ """ -- 2.51.2 From f055925ebe576c348ef2a63de817a2d9704c8ff4 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 21:27:25 -0700 Subject: [PATCH 55/75] lots of small fixes --- simple-stock-bots/simple-discord-stock-bot/bot.py | 15 +++++++++------ .../simple-discord-stock-bot/functions.py | 14 +++++--------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index f6cf617b8..7e5fff12a 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -16,6 +16,7 @@ try: except KeyError: IEX_TOKEN = "" print("Starting without an IEX Token will not allow you to get market data!") +s = Symbol(IEX_TOKEN) client = discord.Client() @@ -24,9 +25,7 @@ intents = discord.Intents.default() intents.members = True bot = commands.Bot( command_prefix="/", - description="Simple bot for getting stock market information. " - + "The bot responds to any ticker in a message with a dollar sign: $tsla. " - + "It also supports the commands below.", + description=s.help_text, intents=intents, ) @@ -126,7 +125,10 @@ async def search(ctx, *, query: str): async def crypto(ctx, symbol: str): """Get the price of a cryptocurrency using in USD.""" reply = s.crypto_reply(symbol) - await ctx.send(reply) + if reply: + await ctx.send(reply) + else: + await ctx.send("Crypto Symbol could not be found.") @bot.command() @@ -165,6 +167,7 @@ async def intra(ctx, sym: str): filename=f"{symbol.upper()}:{datetime.date.today().strftime('%d%b%Y')}.png", ), ) + await ctx.send(f"{s.price_reply([symbol])[symbol]}") @bot.command() @@ -201,12 +204,13 @@ async def chart(ctx, sym: str): filename=f"{symbol.upper()}:{datetime.date.today().strftime('1M%d%b%Y')}.png", ), ) + await ctx.send(f"{s.price_reply([symbol])[symbol]}") @bot.event async def on_message(message): - if message.author == client.user: + if message.author.id == bot.user.id: return if message.content[0] == "/": @@ -222,5 +226,4 @@ async def on_message(message): return -s = Symbol(IEX_TOKEN) bot.run(DISCORD_TOKEN) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 4f04fd7da..3032388fe 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -27,14 +27,14 @@ class Symbol: ) help_text = """ -Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) +Thanks for using this bot, consider supporting it by buying me a beer: https://www.buymeacoffee.com/Anson) -Keep up with the latest news for the bot in itsTelegram Channel: https://t.me/simplestockbotnews +Keep up with the latest news for the bot in its Discord Server: https://discord.gg/VtEHyyTBAK -Full documentation on using and running your own stock bot can be found [here.](https://simple-stock-bots.gitlab.io/site) +Full documentation on using and running your own stock bot can be found here: https://simple-stock-bots.gitlab.io/site **Commands** - - /donate [amount in USD] to donate. 🎗️ + - /donate information on how to donate. 🎗️ - /dividend $[symbol] will return dividend information for the symbol. 📅 - /intra $[symbol] Plot of the stocks movement since the last market open. 📈 - /chart $[symbol] Plot of the stocks movement for the past 1 month. 📊 @@ -59,13 +59,9 @@ Simple Stock Bot is run entirely on donations. All donations go directly towards paying for servers, and market data is provided by [IEX Cloud](https://iexcloud.io/). -The easiest way to donate is to run the `/donate [amount in USD]` command with USdollars you would like to donate. - -Example: `/donate 2` would donate 2 USD. -An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card. +The best way to donate is through https://www.buymeacoffee.com/Anson which accepts Paypal or Credit card. If you have any questions get in touch: MisterBiggs#0465 or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/) -_Donations can only be made in a chat directly with @simplestockbot_ """ def __init__(self, IEX_TOKEN: str) -> None: -- 2.51.2 From fb99c7f134e5d3812acb819b9d0aff537eaf7c34 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 21:29:58 -0700 Subject: [PATCH 56/75] many small bugfixes --- simple-stock-bots/simple-discord-stock-bot/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 3032388fe..027484fdd 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -276,7 +276,7 @@ If you have any questions get in touch: MisterBiggs#0465 or[anson@ansonbiggs.com Each symbol passed in is a key with its value being a human readable formatted string of the symbols div dates. """ - IEXurl = f"https://cloud.iexapis.com/stable/stock/msft/dividends/next?token={self.IEX_TOKEN}" + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" response = r.get(IEXurl) if response.status_code == 200: IEXData = response.json()[0] -- 2.51.2 From 1360cfdf84747de54244432bdc6f85d7f40de521 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 11 Feb 2021 22:27:44 -0700 Subject: [PATCH 57/75] leaked an api key and had wrong intents --- simple-stock-bots/simple-discord-stock-bot/bot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 7e5fff12a..2062971f2 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -9,7 +9,7 @@ from discord.ext import commands from functions import Symbol -DISCORD_TOKEN = "NjYyNzcyOTU5MzMyNTMyMjg2.Xg-1aw.Au28SCJJE3HLy23b3adREtLXFiY" +DISCORD_TOKEN = os.environ["DISCORD"] try: IEX_TOKEN = os.environ["IEX"] @@ -21,12 +21,10 @@ s = Symbol(IEX_TOKEN) client = discord.Client() -intents = discord.Intents.default() -intents.members = True + bot = commands.Bot( command_prefix="/", description=s.help_text, - intents=intents, ) -- 2.51.2 From 291a728591858e6d4e0332c2706d3179c856ea7a Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Fri, 12 Feb 2021 20:55:50 -0700 Subject: [PATCH 58/75] added typing to commands that take longer --- .../simple-discord-stock-bot/bot.py | 121 +++++++++--------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 2062971f2..b9cc69959 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -132,77 +132,78 @@ async def crypto(ctx, symbol: str): @bot.command() async def intra(ctx, sym: str): """Get a chart for the stocks movement since market open.""" + with ctx.channel.typing(): - symbol = s.find_symbols(sym)[0] + symbol = s.find_symbols(sym)[0] - df = s.intra_reply(symbol) - if df.empty: - await ctx.send("Invalid symbol please see `/help` for usage details.") - return + df = s.intra_reply(symbol) + if df.empty: + await ctx.send("Invalid symbol please see `/help` for usage details.") + return + + buf = io.BytesIO() + mpf.plot( + df, + type="renko", + title=f"\n${symbol.upper()}", + volume=True, + style="yahoo", + mav=20, + savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), + ) + buf.seek(0) - buf = io.BytesIO() - mpf.plot( - df, - type="renko", - title=f"\n${symbol.upper()}", - volume=True, - style="yahoo", - mav=20, - savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), - ) - buf.seek(0) - - caption = ( - f"\nIntraday chart for ${symbol.upper()} from {df.first_valid_index().strftime('%I:%M')} to" - + f" {df.last_valid_index().strftime('%I:%M')} ET on" - + f" {datetime.date.today().strftime('%d, %b %Y')}" - ) - - await ctx.send( - content=caption, - file=discord.File( - buf, - filename=f"{symbol.upper()}:{datetime.date.today().strftime('%d%b%Y')}.png", - ), - ) - await ctx.send(f"{s.price_reply([symbol])[symbol]}") + caption = ( + f"\nIntraday chart for ${symbol.upper()} from {df.first_valid_index().strftime('%I:%M')} to" + + f" {df.last_valid_index().strftime('%I:%M')} ET on" + + f" {datetime.date.today().strftime('%d, %b %Y')}" + ) + + await ctx.send( + content=caption, + file=discord.File( + buf, + filename=f"{symbol.upper()}:{datetime.date.today().strftime('%d%b%Y')}.png", + ), + ) + await ctx.send(f"{s.price_reply([symbol])[symbol]}") @bot.command() async def chart(ctx, sym: str): """Get a chart for the stocks movement for the past month.""" + with ctx.channel.typing(): - symbol = s.find_symbols(sym)[0] + symbol = s.find_symbols(sym)[0] - df = s.intra_reply(symbol) - if df.empty: - await ctx.send("Invalid symbol please see `/help` for usage details.") - return + df = s.intra_reply(symbol) + if df.empty: + await ctx.send("Invalid symbol please see `/help` for usage details.") + return + buf = io.BytesIO() + mpf.plot( + df, + type="candle", + title=f"\n${symbol.upper()}", + volume=True, + style="yahoo", + savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), + ) + buf.seek(0) - buf = io.BytesIO() - mpf.plot( - df, - type="candle", - title=f"\n${symbol.upper()}", - volume=True, - style="yahoo", - savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), - ) - buf.seek(0) - - caption = ( - f"\n1 Month chart for ${symbol.upper()} from {df.first_valid_index().strftime('%d, %b %Y')}" - + f" to {df.last_valid_index().strftime('%d, %b %Y')}" - ) - - await ctx.send( - content=caption, - file=discord.File( - buf, - filename=f"{symbol.upper()}:{datetime.date.today().strftime('1M%d%b%Y')}.png", - ), - ) - await ctx.send(f"{s.price_reply([symbol])[symbol]}") + caption = ( + f"\n1 Month chart for ${symbol.upper()} from {df.first_valid_index().strftime('%d, %b %Y')}" + + f" to {df.last_valid_index().strftime('%d, %b %Y')}" + ) + + await ctx.send( + content=caption, + file=discord.File( + buf, + filename=f"{symbol.upper()}:{datetime.date.today().strftime('1M%d%b%Y')}.png", + ), + ) + await ctx.send(f"{s.price_reply([symbol])[symbol]}") @bot.event -- 2.51.2 From 296fe02af438b8ef41e51102880d0e9a3393ed20 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Wed, 24 Feb 2021 23:26:51 -0700 Subject: [PATCH 59/75] =?UTF-8?q?$GME=20=F0=9F=92=8E=F0=9F=99=8C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-stock-bots/simple-discord-stock-bot/functions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py index 027484fdd..0733ba4c0 100644 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ b/simple-stock-bots/simple-discord-stock-bot/functions.py @@ -258,6 +258,9 @@ If you have any questions get in touch: MisterBiggs#0465 or[anson@ansonbiggs.com else: message = f"The symbol: {symbol} was not found." + if symbol.upper() == "GME": + message += "\n\n🙌💎Power to the Players💎🙌" + dataMessages[symbol] = message return dataMessages -- 2.51.2 From f32969bdac3bdaa08543abaa56e2fb3889e5c65f Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 4 Aug 2021 23:14:51 -0700 Subject: [PATCH 60/75] moved everything from telegram over --- .../simple-discord-stock-bot/D_info.py | 75 +++ .../simple-discord-stock-bot/Dockerfile | 4 +- .../simple-discord-stock-bot/IEX_Symbol.py | 531 ++++++++++++++++++ .../simple-discord-stock-bot/Symbol.py | 58 ++ .../simple-discord-stock-bot/bot.py | 2 +- .../simple-discord-stock-bot/cg_Crypto.py | 384 +++++++++++++ .../simple-discord-stock-bot/requirements.txt | 5 +- .../simple-discord-stock-bot/symbol_router.py | 421 ++++++++++++++ 8 files changed, 1475 insertions(+), 5 deletions(-) create mode 100644 simple-stock-bots/simple-discord-stock-bot/D_info.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/Symbol.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/symbol_router.py diff --git a/simple-stock-bots/simple-discord-stock-bot/D_info.py b/simple-stock-bots/simple-discord-stock-bot/D_info.py new file mode 100644 index 000000000..1ba184b4a --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/D_info.py @@ -0,0 +1,75 @@ +"""Functions and Info specific to the Telegram Bot +""" + +import re + +import requests as r + + +class T_info: + license = re.sub( + r"\b\n", + " ", + r.get( + "https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE" + ).text, + ) + + help_text = """ +Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) + +Keep up with the latest news for the bot in its Telegram Channel: https://t.me/simplestockbotnews + +Full documentation on using and running your own stock bot can be found on the bots [docs.](https://docs.simplestockbot.com) + +The bot detects _"Symbols"_ using either one `$` or two `$$` dollar signs before the symbol. One dollar sign is for a stock market ticker, while two is for a cryptocurrency coin. `/chart $$eth` would return a chart of the past month of data for Ethereum, while `/dividend $psec` returns dividend information for Prospect Capital stock. + +Simply calling a symbol in any message that the bot can see will also return the price. So a message like: `I wonder if $$btc will go to the Moon now that $tsla accepts it as payment` would return the current price for both Bitcoin and Tesla. + +**Commands** + - `/donate [amount in USD]` to donate. 🎗️ + - `/dividend $[symbol]` Dividend information for the symbol. 📅 + - `/intra $[symbol]` Plot of the stocks movement since the last market open. 📈 + - `/chart $[symbol]` Plot of the stocks movement for the past 1 month. 📊 + - `/news $[symbol]` News about the symbol. 📰 + - `/info $[symbol]` General information about the symbol. ℹ️ + - `/stat $[symbol]` Key statistics about the symbol. 🔢 + - `/cap $[symbol]` Market Capitalization of symbol. 💰 + - `/trending` Trending Stocks and Cryptos. 💬 + - `/help` Get some help using the bot. 🆘 + +**Inline Features** + You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. Then once you select the ticker want the bot will send a message as you in that chat with the latest stock price. + + Market data is provided by [IEX Cloud](https://iexcloud.io) + + If you believe the bot is not behaving properly run `/status` or [get in touch](https://docs.simplestockbot.com/contact). + """ + + donate_text = """ +Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson) +All donations go directly towards paying for servers, and market data is provided by +[IEX Cloud](https://iexcloud.io/). + +The easiest way to donate is to run the `/donate [amount in USD]` command with US dollars you would like to donate. + +Example: `/donate 2` would donate 2 USD. + +An alternative way to donate is through https://www.buymeacoffee.com/Anson which requires no account and accepts Paypal or Credit card. +If you have any questions see the [website](https:docs.simplestockbot.com) + + """ + + +commands = """ +donate - Donate to the bot 🎗️ +help - Get some help using the bot. 🆘 +info - $[symbol] General information about the symbol. ℹ️ +news - $[symbol] News about the symbol. 📰 +stat - $[symbol] Key statistics about the symbol. 🔢 +cap - $[symbol] Market Capitalization of symbol. 💰 +dividend - $[symbol] Dividend info 📅 +trending - Trending Stocks and Cryptos. 💬 +intra - $[symbol] Plot since the last market open. 📈 +chart - $[chart] Plot of the past month. 📊 +""" # Not used by the bot but for updaing commands with BotFather diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile index eb3699110..2fe1d1d4a 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -3,6 +3,6 @@ FROM python:3.8-buster COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -COPY . . +# COPY . . -CMD [ "python", "./bot.py" ] +# CMD [ "python", "./bot.py" ] diff --git a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py new file mode 100644 index 000000000..87d16f3b7 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py @@ -0,0 +1,531 @@ +"""Class with functions for running the bot with IEX Cloud. +""" + +import os +from datetime import datetime +from logging import warning +from typing import List, Optional, Tuple + +import pandas as pd +import requests as r +import schedule +from fuzzywuzzy import fuzz + +from Symbol import Stock + + +class IEX_Symbol: + """ + Functions for finding stock market information about symbols. + """ + + SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" + + searched_symbols = {} + otc_list = [] + charts = {} + + def __init__(self) -> None: + """Creates a Symbol Object + + Parameters + ---------- + IEX_TOKEN : str + IEX API Token + """ + try: + self.IEX_TOKEN = os.environ["IEX"] + except KeyError: + self.IEX_TOKEN = "" + warning( + "Starting without an IEX Token will not allow you to get market data!" + ) + + if self.IEX_TOKEN != "": + self.get_symbol_list() + + schedule.every().day.do(self.get_symbol_list) + schedule.every().day.do(self.clear_charts) + + def clear_charts(self) -> None: + """ + Clears cache of chart data. + Charts are cached so that only 1 API call per 24 hours is needed since the + chart data is expensive and a large download. + """ + self.charts = {} + + def get_symbol_list( + self, return_df=False + ) -> Optional[Tuple[pd.DataFrame, datetime]]: + """Gets list of all symbols supported by IEX + + Parameters + ---------- + return_df : bool, optional + return the dataframe of all stock symbols, by default False + + Returns + ------- + Optional[Tuple[pd.DataFrame, datetime]] + If `return_df` is set to `True` returns a dataframe, otherwise returns `None`. + """ + + reg_symbols = r.get( + f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}", + timeout=5, + ).json() + otc_symbols = r.get( + f"https://cloud.iexapis.com/stable/ref-data/otc/symbols?token={self.IEX_TOKEN}", + timeout=5, + ).json() + + reg = pd.DataFrame(data=reg_symbols) + otc = pd.DataFrame(data=otc_symbols) + self.otc_list = set(otc["symbol"].to_list()) + + symbols = pd.concat([reg, otc]) + + symbols["description"] = "$" + symbols["symbol"] + ": " + symbols["name"] + symbols["id"] = symbols["symbol"] + symbols["type_id"] = "$" + symbols["symbol"].str.lower() + + symbols = symbols[["id", "symbol", "name", "description", "type_id"]] + self.symbol_list = symbols + if return_df: + return symbols, datetime.now() + + def status(self) -> str: + """Checks IEX Status dashboard for any current API issues. + + Returns + ------- + str + Human readable text on status of IEX API + """ + resp = r.get( + "https://pjmps0c34hp7.statuspage.io/api/v2/status.json", + timeout=5, + ) + + if resp.status_code == 200: + status = resp.json()["status"] + else: + return "IEX Cloud did not respond. Please check their status page for more information. https://status.iexapis.com" + + if status["indicator"] == "none": + return "IEX Cloud is currently not reporting any issues with its API." + else: + return ( + f"{status['indicator']}: {status['description']}." + + " Please check the status page for more information. https://status.iexapis.com" + ) + + def search_symbols(self, search: str) -> List[Tuple[str, str]]: + """Performs a fuzzy search to find stock symbols closest to a search term. + + Parameters + ---------- + search : str + String used to search, could be a company name or something close to the companies stock ticker. + + Returns + ------- + List[tuple[str, str]] + A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + """ + + schedule.run_pending() + search = search.lower() + try: # https://stackoverflow.com/a/3845776/8774114 + return self.searched_symbols[search] + except KeyError: + pass + + symbols = self.symbol_list + symbols["Match"] = symbols.apply( + lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), + axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + if symbols["Match"].head().sum() < 300: + symbols["Match"] = symbols.apply( + lambda x: fuzz.partial_ratio(search, x["name"].lower()), + axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + symbols = symbols.head(10) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def price_reply(self, symbol: Stock) -> str: + """Returns price movement of Stock for the last market day, or after hours. + + Parameters + ---------- + symbol : Stock + + Returns + ------- + str + Formatted markdown + """ + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" + + response = r.get( + IEXurl, + timeout=5, + ) + if response.status_code == 200: + IEXData = response.json() + + if symbol.symbol.upper() in self.otc_list: + return f"OTC - {symbol.symbol.upper()}, {IEXData['companyName']} most recent price is: $**{IEXData['latestPrice']}**" + + keys = ( + "extendedChangePercent", + "extendedPrice", + "companyName", + "latestPrice", + "changePercent", + ) + + if set(keys).issubset(IEXData): + + if change := IEXData.get("changePercent", 0): + change = round(change * 100, 2) + else: + change = 0 + + if ( + IEXData.get("isUSMarketOpen", True) + or (IEXData["extendedChangePercent"] is None) + or (IEXData["extendedPrice"] is None) + ): # Check if market is open. + message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" + else: + message = ( + f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}** with a change of {change}%," + + f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**" + ) + if change := IEXData.get("extendedChangePercent", 0): + change = round(change * 100, 2) + else: + change = 0 + + # Determine wording of change text + if change > 0: + message += f", the stock is currently **up {change}%**" + elif change < 0: + message += f", the stock is currently **down {change}%**" + else: + message += ", the stock hasn't shown any movement today." + else: + message = ( + f"The symbol: {symbol} encountered and error. This could be due to " + ) + + else: + message = f"The symbol: {symbol} was not found." + + return message + + def dividend_reply(self, symbol: Stock) -> str: + """Returns the most recent, or next dividend date for a stock symbol. + + Parameters + ---------- + symbol : Stock + + Returns + ------- + str + Formatted markdown + """ + if symbol.symbol.upper() in self.otc_list: + return "OTC stocks do not currently support any commands." + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" + response = r.get( + IEXurl, + timeout=5, + ) + if response.status_code == 200 and response.json(): + IEXData = response.json()[0] + keys = ( + "amount", + "currency", + "declaredDate", + "exDate", + "frequency", + "paymentDate", + "flag", + ) + + if set(keys).issubset(IEXData): + + if IEXData["currency"] == "USD": + price = f"${IEXData['amount']}" + else: + price = f"{IEXData['amount']} {IEXData['currency']}" + + # Pattern IEX uses for dividend date. + pattern = "%Y-%m-%d" + + declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime( + "%A, %B %w" + ) + ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w") + payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime( + "%A, %B %w" + ) + + daysDelta = ( + datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now() + ).days + + return ( + "The next dividend for " + + f"{self.symbol_list[self.symbol_list['symbol']==symbol.id.upper()]['description'].item()}" # Get full name without api call + + f" is on {payment} which is in {daysDelta} days." + + f" The dividend is for {price} per share." + + f"\n\nThe dividend was declared on {declared} and the ex-dividend date is {ex}" + ) + + return f"${symbol.id.upper()} either doesn't exist or pays no dividend." + + def news_reply(self, symbol: Stock) -> str: + """Gets most recent, english, non-paywalled news + + Parameters + ---------- + symbol : Stock + + Returns + ------- + str + Formatted markdown + """ + if symbol.symbol.upper() in self.otc_list: + return "OTC stocks do not currently support any commands." + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/15?token={self.IEX_TOKEN}" + response = r.get( + IEXurl, + timeout=5, + ) + if response.status_code == 200: + data = response.json() + if data: + line = [] + + for news in data: + if news["lang"] == "en" and not news["hasPaywall"]: + line.append( + f"*{news['source']}*: [{news['headline']}]({news['url']})" + ) + + else: + return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." + else: + return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." + + return f"News for **{symbol.id.upper()}**:\n" + "\n".join(line[:5]) + + def info_reply(self, symbol: Stock) -> str: + """Gets description for Stock + + Parameters + ---------- + symbol : Stock + + Returns + ------- + str + Formatted text + """ + if symbol.symbol.upper() in self.otc_list: + return "OTC stocks do not currently support any commands." + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" + response = r.get( + IEXurl, + timeout=5, + ) + + if response.status_code == 200: + data = response.json() + [data.pop(k) for k in list(data) if data[k] == ""] + + if "description" in data: + return data["description"] + + return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + + def stat_reply(self, symbol: Stock) -> str: + """Key statistics on a Stock + + Parameters + ---------- + symbol : Stock + + Returns + ------- + str + Formatted markdown + """ + if symbol.symbol.upper() in self.otc_list: + return "OTC stocks do not currently support any commands." + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" + response = r.get( + IEXurl, + timeout=5, + ) + + if response.status_code == 200: + data = response.json() + [data.pop(k) for k in list(data) if data[k] == ""] + + m = "" + if "companyName" in data: + m += f"Company Name: {data['companyName']}\n" + if "marketcap" in data: + m += f"Market Cap: ${data['marketcap']:,}\n" + if "week52high" in data: + m += f"52 Week (high-low): {data['week52high']:,} " + if "week52low" in data: + m += f"- {data['week52low']:,}\n" + if "employees" in data: + m += f"Number of Employees: {data['employees']:,}\n" + if "nextEarningsDate" in data: + m += f"Next Earnings Date: {data['nextEarningsDate']}\n" + if "peRatio" in data: + m += f"Price to Earnings: {data['peRatio']:.3f}\n" + if "beta" in data: + m += f"Beta: {data['beta']:.3f}\n" + return m + else: + return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + + def cap_reply(self, stock: Stock) -> str: + """Get the Market Cap of a stock""" + response = r.get( + f"https://cloud.iexapis.com/stable/stock/{stock.id}/stats?token={self.IEX_TOKEN}", + timeout=5, + ) + if response.status_code == 200: + + try: + data = response.json() + + cap = data["marketcap"] + except KeyError: + return f"{stock.id} returned an error." + + message = f"The current market cap of {stock.name} is $**{cap:,.2f}**" + + else: + message = f"The Coin: {stock.name} was not found or returned and error." + + return message + + def intra_reply(self, symbol: Stock) -> pd.DataFrame: + """Returns price data for a symbol since the last market open. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + if symbol.symbol.upper() in self.otc_list: + return pd.DataFrame() + + if symbol.id.upper() not in list(self.symbol_list["symbol"]): + return pd.DataFrame() + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}" + response = r.get( + IEXurl, + timeout=5, + ) + if response.status_code == 200: + df = pd.DataFrame(response.json()) + df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) + df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) + df = df.set_index("DT") + return df + + return pd.DataFrame() + + def chart_reply(self, symbol: Stock) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + schedule.run_pending() + + if symbol.symbol.upper() in self.otc_list: + return pd.DataFrame() + + if symbol.id.upper() not in list(self.symbol_list["symbol"]): + return pd.DataFrame() + + try: # https://stackoverflow.com/a/3845776/8774114 + return self.charts[symbol.id.upper()] + except KeyError: + pass + + response = r.get( + f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false", + timeout=5, + ) + + if response.status_code == 200: + df = pd.DataFrame(response.json()) + df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) + df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) + df = df.set_index("DT") + self.charts[symbol.id.upper()] = df + return df + + return pd.DataFrame() + + def trending(self) -> list[str]: + """Gets current coins trending on IEX. Only returns when market is open. + + Returns + ------- + list[str] + list of $ID: NAME, CHANGE% + """ + + stocks = r.get( + f"https://cloud.iexapis.com/stable/stock/market/list/mostactive?token={self.IEX_TOKEN}", + timeout=5, + ) + if stocks.status_code == 200: + return [ + f"`${s['symbol']}`: {s['companyName']}, {100*s['changePercent']:.2f}%" + for s in stocks.json() + ] + else: + return ["Trending Stocks Currently Unavailable."] diff --git a/simple-stock-bots/simple-discord-stock-bot/Symbol.py b/simple-stock-bots/simple-discord-stock-bot/Symbol.py new file mode 100644 index 000000000..db04b4a2a --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/Symbol.py @@ -0,0 +1,58 @@ +import functools + +import requests as r + + +class Symbol: + """ + symbol: What the user calls it. ie tsla or btc + id: What the api expects. ie tsla or bitcoin + name: Human readable. ie Tesla or Bitcoin + """ + + currency = "usd" + pass + + def __init__(self, symbol) -> None: + self.symbol = symbol + self.id = symbol + self.name = symbol + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} instance of {self.id} at {id(self)}>" + + def __str__(self) -> str: + return self.id + + +class Stock(Symbol): + """Stock Market Object. Gets data from IEX Cloud""" + + def __init__(self, symbol: str) -> None: + self.symbol = symbol + self.id = symbol + self.name = "$" + symbol.upper() + + +# Used by Coin to change symbols for ids +coins = r.get("https://api.coingecko.com/api/v3/coins/list").json() + + +class Coin(Symbol): + """Cryptocurrency Object. Gets data from CoinGecko.""" + + @functools.cache + def __init__(self, symbol: str) -> None: + self.symbol = symbol + self.get_data() + + def get_data(self) -> None: + self.id = list(filter(lambda coin: coin["symbol"] == self.symbol, coins))[0][ + "id" + ] + data = r.get("https://api.coingecko.com/api/v3/coins/" + self.id).json() + self.data = data + + self.name = data["name"] + self.description = data["description"] + # self.price = data["market_data"]["current_price"][self.currency] diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index b9cc69959..6a73c4faa 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -9,7 +9,7 @@ from discord.ext import commands from functions import Symbol -DISCORD_TOKEN = os.environ["DISCORD"] +DISCORD_TOKEN = "NjAxNjYwNzM1ODQzNDY3Mjg0.XTFiOQ.DMEKIug2mhX1WGGVSD1bYuYXVro" try: IEX_TOKEN = os.environ["IEX"] diff --git a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py new file mode 100644 index 000000000..00f248c96 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py @@ -0,0 +1,384 @@ +"""Class with functions for running the bot with IEX Cloud. +""" + +from datetime import datetime +from typing import List, Optional, Tuple + +import pandas as pd +import requests as r +import schedule +from fuzzywuzzy import fuzz +from markdownify import markdownify + +from Symbol import Coin + + +class cg_Crypto: + """ + Functions for finding crypto info + """ + + vs_currency = "usd" # simple/supported_vs_currencies for list of options + + searched_symbols = {} + + def __init__(self) -> None: + """Creates a Symbol Object + + Parameters + ---------- + IEX_TOKEN : str + IEX Token + """ + self.get_symbol_list() + schedule.every().day.do(self.get_symbol_list) + + def symbol_id(self, symbol) -> str: + try: + return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[ + 0 + ] + except KeyError: + return "" + + def get_symbol_list( + self, return_df=False + ) -> Optional[Tuple[pd.DataFrame, datetime]]: + + raw_symbols = r.get( + "https://api.coingecko.com/api/v3/coins/list", + timeout=5, + ).json() + symbols = pd.DataFrame(data=raw_symbols) + + symbols["description"] = ( + "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"] + ) + symbols = symbols[["id", "symbol", "name", "description"]] + symbols["type_id"] = "$$" + symbols["id"] + + self.symbol_list = symbols + if return_df: + return symbols, datetime.now() + + def status(self) -> str: + """Checks CoinGecko /ping endpoint for API issues. + + Returns + ------- + str + Human readable text on status of CoinGecko API + """ + status = r.get( + "https://api.coingecko.com/api/v3/ping", + timeout=5, + ) + + if status.status_code == 200: + return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." + else: + return f"CoinGecko API returned an error in {status.elapsed.total_seconds()} Seconds." + + def search_symbols(self, search: str) -> List[Tuple[str, str]]: + """Performs a fuzzy search to find coin symbols closest to a search term. + + Parameters + ---------- + search : str + String used to search, could be a company name or something close to the companies coin ticker. + + Returns + ------- + List[tuple[str, str]] + A list tuples of every coin sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + """ + schedule.run_pending() + search = search.lower() + try: # https://stackoverflow.com/a/3845776/8774114 + return self.searched_symbols[search] + except KeyError: + pass + + symbols = self.symbol_list + symbols["Match"] = symbols.apply( + lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), + axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + if symbols["Match"].head().sum() < 300: + symbols["Match"] = symbols.apply( + lambda x: fuzz.partial_ratio(search, x["name"].lower()), + axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + symbols = symbols.head(10) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def price_reply(self, coin: Coin) -> str: + """Returns current market price or after hours if its available for a given coin symbol. + + Parameters + ---------- + symbols : list + List of coin symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + markdown formatted string of the symbols price and movement. + """ + + response = r.get( + f"https://api.coingecko.com/api/v3/simple/price?ids={coin.id}&vs_currencies={self.vs_currency}&include_24hr_change=true", + timeout=5, + ) + if response.status_code == 200: + + try: + data = response.json()[coin.id] + + price = data[self.vs_currency] + change = data[self.vs_currency + "_24h_change"] + if change is None: + change = 0 + except KeyError: + return f"{coin.id} returned an error." + + message = f"The current price of {coin.name} is $**{price:,}**" + + # Determine wording of change text + if change > 0: + message += f", the coin is currently **up {change:.3f}%** for today" + elif change < 0: + message += f", the coin is currently **down {change:.3f}%** for today" + else: + message += ", the coin hasn't shown any movement today." + + else: + message = f"The Coin: {coin.name} was not found." + + return message + + def intra_reply(self, symbol: Coin) -> pd.DataFrame: + """Returns price data for a symbol since the last market open. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + response = r.get( + f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1", + timeout=5, + ) + if response.status_code == 200: + df = pd.DataFrame( + response.json(), columns=["Date", "Open", "High", "Low", "Close"] + ).dropna() + df["Date"] = pd.to_datetime(df["Date"], unit="ms") + df = df.set_index("Date") + return df + + return pd.DataFrame() + + def chart_reply(self, symbol: Coin) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + response = r.get( + f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30", + timeout=5, + ) + + if response.status_code == 200: + df = pd.DataFrame( + response.json(), columns=["Date", "Open", "High", "Low", "Close"] + ).dropna() + df["Date"] = pd.to_datetime(df["Date"], unit="ms") + df = df.set_index("Date") + return df + + return pd.DataFrame() + + def stat_reply(self, symbol: Coin) -> str: + """Gathers key statistics on coin. Mostly just CoinGecko scores. + + Parameters + ---------- + symbol : Coin + + Returns + ------- + str + Preformatted markdown. + """ + + response = r.get( + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", + timeout=5, + ) + if response.status_code == 200: + data = response.json() + + return f""" + [{data['name']}]({data['links']['homepage'][0]}) Statistics: + Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,} + Market Cap Ranking: {data.get('market_cap_rank',"Not Available")} + CoinGecko Scores: + Overall: {data.get('coingecko_score','Not Available')} + Development: {data.get('developer_score','Not Available')} + Community: {data.get('community_score','Not Available')} + Public Interest: {data.get('public_interest_score','Not Available')} + """ + else: + return f"{symbol.symbol} returned an error." + + def cap_reply(self, coin: Coin) -> str: + """Gets market cap for Coin + + Parameters + ---------- + coin : Coin + + Returns + ------- + str + Preformatted markdown. + """ + response = r.get( + f"https://api.coingecko.com/api/v3/simple/price?ids={coin.id}&vs_currencies={self.vs_currency}&include_market_cap=true", + timeout=5, + ) + if response.status_code == 200: + + try: + data = response.json()[coin.id] + + price = data[self.vs_currency] + cap = data[self.vs_currency + "_market_cap"] + except KeyError: + return f"{coin.id} returned an error." + + if cap == 0: + return f"The market cap for {coin.name} is not available for unknown reasons." + + message = f"The current price of {coin.name} is $**{price:,}** and its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}" + + else: + message = f"The Coin: {coin.name} was not found or returned and error." + + return message + + def info_reply(self, symbol: Coin) -> str: + """Gets coin description + + Parameters + ---------- + symbol : Coin + + Returns + ------- + str + Preformatted markdown. + """ + + response = r.get( + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", + timeout=5, + ) + if response.status_code == 200: + data = response.json() + try: + return markdownify(data["description"]["en"]) + except KeyError: + return f"{symbol} does not have a description available." + + return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + + def trending(self) -> list[str]: + """Gets current coins trending on coingecko + + Returns + ------- + list[str] + list of $$ID: NAME, CHANGE% + """ + + coins = r.get( + "https://api.coingecko.com/api/v3/search/trending", + timeout=5, + ) + try: + trending = [] + if coins.status_code == 200: + for coin in coins.json()["coins"]: + c = coin["item"] + + sym = c["symbol"].upper() + name = c["name"] + change = r.get( + f"https://api.coingecko.com/api/v3/simple/price?ids={c['id']}&vs_currencies={self.vs_currency}&include_24hr_change=true" + ).json()[c["id"]]["usd_24h_change"] + + msg = f"`$${sym}`: {name}, {change:.2f}%" + + trending.append(msg) + + except Exception as e: + print(e) + trending = ["Trending Coins Currently Unavailable."] + + return trending + + def batch_price(self, coins: list[Coin]) -> list[str]: + """Gets price of a list of coins all in one API call + + Parameters + ---------- + coins : list[Coin] + + Returns + ------- + list[str] + returns preformatted list of strings detailing price movement of each coin passed in. + """ + query = ",".join([c.id for c in coins]) + + prices = r.get( + f"https://api.coingecko.com/api/v3/simple/price?ids={query}&vs_currencies=usd&include_24hr_change=true", + timeout=5, + ).json() + + replies = [] + for coin in coins: + if coin.id in prices: + p = prices[coin.id] + + if p.get("usd_24h_change") is None: + p["usd_24h_change"] = 0 + + replies.append( + f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours." + ) + + return replies diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 021548087..7b7e5d0f1 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,7 +1,8 @@ -discord.py==1.6 +discord.py==1.7.3 requests==2.25.1 pandas==1.2.1 fuzzywuzzy==0.18.0 python-Levenshtein==0.12.1 schedule==1.0.0 -mplfinance==0.12.7a5 \ No newline at end of file +mplfinance==0.12.7a5 +markdownify==0.6.5 \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/symbol_router.py b/simple-stock-bots/simple-discord-stock-bot/symbol_router.py new file mode 100644 index 000000000..efcecd4d9 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/symbol_router.py @@ -0,0 +1,421 @@ +"""Function that routes symbols to the correct API provider. +""" + +import datetime +import random +import re +from logging import critical, debug, error, info, warning + +import pandas as pd +from fuzzywuzzy import fuzz + +from cg_Crypto import cg_Crypto +from IEX_Symbol import IEX_Symbol +from Symbol import Coin, Stock, Symbol + + +class Router: + STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})" + CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})" + searched_symbols = {} + + def __init__(self): + self.stock = IEX_Symbol() + self.crypto = cg_Crypto() + + def find_symbols(self, text: str) -> list[Symbol]: + """Finds stock tickers starting with a dollar sign, and cryptocurrencies with two dollar signs + in a blob of text and returns them in a list. + + Parameters + ---------- + text : str + Blob of text. + + Returns + ------- + list[str] + List of stock symbols as strings without dollar sign. + """ + symbols = [] + stocks = set(re.findall(self.STOCK_REGEX, text)) + for stock in stocks: + if stock.upper() in self.stock.symbol_list["symbol"].values: + symbols.append(Stock(stock)) + else: + info(f"{stock} is not in list of stocks") + + coins = set(re.findall(self.CRYPTO_REGEX, text)) + for coin in coins: + if coin.lower() in self.crypto.symbol_list["symbol"].values: + symbols.append(Coin(coin.lower())) + else: + info(f"{coin} is not in list of coins") + info(symbols) + return symbols + + def status(self, bot_resp) -> str: + """Checks for any issues with APIs. + + Returns + ------- + str + Human readable text on status of the bot and relevant APIs + """ + + stats = f""" + Bot Status: + {bot_resp} + + Stock Market Data: + {self.stock.status()} + + Cryptocurrency Data: + {self.crypto.status()} + """ + + warning(stats) + + return stats + + def search_symbols(self, search: str) -> list[tuple[str, str]]: + """Performs a fuzzy search to find stock symbols closest to a search term. + + Parameters + ---------- + search : str + String used to search, could be a company name or something close to the companies stock ticker. + + Returns + ------- + list[tuple[str, str]] + A list tuples of every stock sorted in order of how well they match. + Each tuple contains: (Symbol, Issue Name). + """ + + df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) + + search = search.lower() + + df["Match"] = df.apply( + lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), + axis=1, + ) + + df.sort_values(by="Match", ascending=False, inplace=True) + # if df["Match"].head().sum() < 300: + # df["Match"] = df.apply( + # lambda x: fuzz.partial_ratio(search, x["name"].lower()), + # axis=1, + # ) + + # df.sort_values(by="Match", ascending=False, inplace=True) + + symbols = df.head(20) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def inline_search(self, search: str) -> list[tuple[str, str]]: + """Searches based on the shortest symbol that contains the same string as the search. + Should be very fast compared to a fuzzy search. + + Parameters + ---------- + search : str + String used to match against symbols. + + Returns + ------- + list[tuple[str, str]] + Each tuple contains: (Symbol, Issue Name). + """ + + df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) + + search = search.lower() + + df = df[df["type_id"].str.contains(search, regex=False)].sort_values( + by="type_id", key=lambda x: x.str.len() + ) + + symbols = df.head(20) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def price_reply(self, symbols: list[Symbol]) -> list[str]: + """Returns current market price or after hours if its available for a given stock symbol. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + markdown formatted string of the symbols price and movement. + """ + replies = [] + + for symbol in symbols: + info(symbol) + if isinstance(symbol, Stock): + replies.append(self.stock.price_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append(self.crypto.price_reply(symbol)) + else: + info(f"{symbol} is not a Stock or Coin") + + return replies + + def dividend_reply(self, symbols: list) -> list[str]: + """Returns the most recent, or next dividend date for a stock symbol. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + formatted string of the symbols div dates. + """ + replies = [] + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.dividend_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append("Cryptocurrencies do no have Dividends.") + else: + print(f"{symbol} is not a Stock or Coin") + + return replies + + def news_reply(self, symbols: list) -> list[str]: + """Gets recent english news on stock symbols. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human + readable markdown formatted string of the symbols news. + """ + replies = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.news_reply(symbol)) + elif isinstance(symbol, Coin): + # replies.append(self.crypto.news_reply(symbol)) + replies.append( + "News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs" + ) + else: + print(f"{symbol} is not a Stock or Coin") + + return replies + + def info_reply(self, symbols: list) -> list[str]: + """Gets information on stock symbols. + + Parameters + ---------- + symbols : list[str] + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted + string of the symbols information. + """ + replies = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.info_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append(self.crypto.info_reply(symbol)) + else: + print(f"{symbol} is not a Stock or Coin") + + return replies + + def intra_reply(self, symbol: Symbol) -> pd.DataFrame: + """Returns price data for a symbol since the last market open. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. + Otherwise returns empty pd.DataFrame. + """ + + if isinstance(symbol, Stock): + return self.stock.intra_reply(symbol) + elif isinstance(symbol, Coin): + return self.crypto.intra_reply(symbol) + else: + print(f"{symbol} is not a Stock or Coin") + return pd.DataFrame() + + def chart_reply(self, symbol: Symbol) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. + Otherwise returns empty pd.DataFrame. + """ + if isinstance(symbol, Stock): + return self.stock.chart_reply(symbol) + elif isinstance(symbol, Coin): + return self.crypto.chart_reply(symbol) + else: + print(f"{symbol} is not a Stock or Coin") + return pd.DataFrame() + + def stat_reply(self, symbols: list[Symbol]) -> list[str]: + """Gets key statistics for each symbol in the list + + Parameters + ---------- + symbols : list[str] + List of stock symbols + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + formatted string of the symbols statistics. + """ + replies = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.stat_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append(self.crypto.stat_reply(symbol)) + else: + print(f"{symbol} is not a Stock or Coin") + + return replies + + def cap_reply(self, symbols: list[Symbol]) -> list[str]: + """Gets market cap for each symbol in the list + + Parameters + ---------- + symbols : list[str] + List of stock symbols + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + formatted string of the symbols market cap. + """ + replies = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.cap_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append(self.crypto.cap_reply(symbol)) + else: + print(f"{symbol} is not a Stock or Coin") + + return replies + + def trending(self) -> str: + """Checks APIs for trending symbols. + + Returns + ------- + list[str] + List of preformatted strings to be sent to user. + """ + + stocks = self.stock.trending() + coins = self.crypto.trending() + + reply = "Trending Stocks:\n" + reply += "-" * len("Trending Stocks:") + "\n" + for stock in stocks: + reply += stock + "\n" + + reply += "\n\nTrending Crypto:\n" + reply += "-" * len("Trending Crypto:") + "\n" + for coin in coins: + reply += coin + "\n" + + return reply + + def random_pick(self) -> str: + + choice = random.choice( + list(self.stock.symbol_list["description"]) + + list(self.crypto.symbol_list["description"]) + ) + hold = ( + datetime.date.today() + datetime.timedelta(random.randint(1, 365)) + ).strftime("%b %d, %Y") + + return f"{choice}\nBuy and hold until: {hold}" + + def batch_price_reply(self, symbols: list[Symbol]) -> list[str]: + """Returns current market price or after hours if its available for a given stock symbol. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + markdown formatted string of the symbols price and movement. + """ + replies = [] + stocks = [] + coins = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + stocks.append(symbol) + elif isinstance(symbol, Coin): + coins.append(symbol) + else: + print(f"{symbol} is not a Stock or Coin") + + if stocks: + # IEX batch endpoint doesnt seem to be working right now + for stock in stocks: + replies.append(self.stock.price_reply(stock)) + if coins: + replies = replies + self.crypto.batch_price(coins) + + return replies -- 2.51.2 From 5dcfd804f8387d7d1c42339ae056c2f221b48bf6 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 4 Aug 2021 23:19:13 -0700 Subject: [PATCH 61/75] small changes --- simple-stock-bots/simple-discord-stock-bot/.gitignore | 1 + simple-stock-bots/simple-discord-stock-bot/D_info.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitignore b/simple-stock-bots/simple-discord-stock-bot/.gitignore index be952f709..bd1d97038 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.gitignore +++ b/simple-stock-bots/simple-discord-stock-bot/.gitignore @@ -1,2 +1,3 @@ .vscode/settings.json __pycache__/functions.cpython-38.pyc +.devcontainer \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/D_info.py b/simple-stock-bots/simple-discord-stock-bot/D_info.py index 1ba184b4a..a89efdc4f 100644 --- a/simple-stock-bots/simple-discord-stock-bot/D_info.py +++ b/simple-stock-bots/simple-discord-stock-bot/D_info.py @@ -11,7 +11,7 @@ class T_info: r"\b\n", " ", r.get( - "https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE" + "https://gitlab.com/simple-stock-bots/simple-discord-stock-bot/-/raw/master/LICENSE" ).text, ) @@ -38,8 +38,6 @@ Simply calling a symbol in any message that the bot can see will also return the - `/trending` Trending Stocks and Cryptos. 💬 - `/help` Get some help using the bot. 🆘 -**Inline Features** - You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. Then once you select the ticker want the bot will send a message as you in that chat with the latest stock price. Market data is provided by [IEX Cloud](https://iexcloud.io) @@ -56,7 +54,7 @@ The easiest way to donate is to run the `/donate [amount in USD]` command with U Example: `/donate 2` would donate 2 USD. An alternative way to donate is through https://www.buymeacoffee.com/Anson which requires no account and accepts Paypal or Credit card. -If you have any questions see the [website](https:docs.simplestockbot.com) +If you have any questions see the [website](https://docs.simplestockbot.com) """ -- 2.51.2 From 916cde3585ca3825b260afcd6c4d1850ff3e0301 Mon Sep 17 00:00:00 2001 From: Anson Date: Thu, 5 Aug 2021 01:18:14 -0700 Subject: [PATCH 62/75] updated to meet telegram features --- .../simple-discord-stock-bot/.gitignore | 2 +- .../simple-discord-stock-bot/D_info.py | 2 +- .../simple-discord-stock-bot/Dockerfile | 6 +- .../simple-discord-stock-bot/bot.py | 193 ++++++++++-------- 4 files changed, 113 insertions(+), 90 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitignore b/simple-stock-bots/simple-discord-stock-bot/.gitignore index bd1d97038..5e5820c48 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.gitignore +++ b/simple-stock-bots/simple-discord-stock-bot/.gitignore @@ -1,3 +1,3 @@ .vscode/settings.json -__pycache__/functions.cpython-38.pyc +__pycache__ .devcontainer \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/D_info.py b/simple-stock-bots/simple-discord-stock-bot/D_info.py index a89efdc4f..5ccb85efd 100644 --- a/simple-stock-bots/simple-discord-stock-bot/D_info.py +++ b/simple-stock-bots/simple-discord-stock-bot/D_info.py @@ -6,7 +6,7 @@ import re import requests as r -class T_info: +class D_info: license = re.sub( r"\b\n", " ", diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile index 2fe1d1d4a..2de21c081 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.8-buster +FROM python:3.9-buster COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -# COPY . . +COPY . . -# CMD [ "python", "./bot.py" ] +CMD [ "python", "./bot.py" ] diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 6a73c4faa..0d596da19 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,22 +1,29 @@ import datetime +import html import io +import json +import logging import os +import random +import string +import traceback +from logging import critical, debug, error, info, warning +from uuid import uuid4 import mplfinance as mpf + import discord from discord.ext import commands -from functions import Symbol +from symbol_router import Router +from D_info import D_info + -DISCORD_TOKEN = "NjAxNjYwNzM1ODQzNDY3Mjg0.XTFiOQ.DMEKIug2mhX1WGGVSD1bYuYXVro" +DISCORD_TOKEN = os.environ["DISCORD"] -try: - IEX_TOKEN = os.environ["IEX"] -except KeyError: - IEX_TOKEN = "" - print("Starting without an IEX Token will not allow you to get market data!") -s = Symbol(IEX_TOKEN) +s = Router() +d = D_info() client = discord.Client() @@ -24,7 +31,7 @@ client = discord.Client() bot = commands.Bot( command_prefix="/", - description=s.help_text, + description=d.help_text, ) @@ -38,16 +45,12 @@ async def on_ready(): @bot.command() -async def status(ctx): +async def status(ctx: commands): """Debug command for diagnosing if the bot is experiencing any issues.""" message = "" try: message = "Contact MisterBiggs#0465 if you need help.\n" - # IEX Status - message += s.iex_status() + "\n" - - # Message Status - message += s.message_status() + message += s.status("") + "\n" except Exception as ex: message += ( f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n" @@ -57,59 +60,59 @@ async def status(ctx): @bot.command() -async def license(ctx): +async def license(ctx: commands): """Returns the bots license agreement.""" - await ctx.send(s.license) + await ctx.send(d.license) @bot.command() -async def donate(ctx): +async def donate(ctx: commands): """Details on how to support the development and hosting of the bot.""" - await ctx.send(s.donate_text) + await ctx.send(d.donate_text) @bot.command() -async def stat(ctx, *, sym: str): +async def stat(ctx: commands, *, sym: str): """Get statistics on a list of stock symbols.""" symbols = s.find_symbols(sym) if symbols: - for reply in s.stat_reply(symbols).items(): - await ctx.send(reply[1]) + for reply in s.stat_reply(symbols): + await ctx.send(reply) @bot.command() -async def dividend(ctx, *, sym: str): +async def dividend(ctx: commands, *, sym: str): """Get dividend information on a stock symbol.""" symbols = s.find_symbols(sym) if symbols: - for symbol in symbols: - await ctx.send(s.dividend_reply(symbol)) + for reply in s.dividend_reply(symbols): + await ctx.send(reply) @bot.command() -async def news(ctx, *, sym: str): +async def news(ctx: commands, *, sym: str): """Get recent english news on a stock symbol.""" symbols = s.find_symbols(sym) if symbols: - for reply in s.news_reply(symbols).items(): - await ctx.send(reply[1]) + for reply in s.news_reply(symbols): + await ctx.send(reply) @bot.command() -async def info(ctx, *, sym: str): +async def info(ctx: commands, *, sym: str): """Get information of a stock ticker.""" symbols = s.find_symbols(sym) if symbols: - for reply in s.info_reply(symbols).items(): - await ctx.send(reply[1]) + for reply in s.info_reply(symbols): + await ctx.send(reply[0:1900]) @bot.command() -async def search(ctx, *, query: str): +async def search(ctx: commands, *, query: str): """Search for a stock symbol using either symbol of company name.""" results = s.search_symbols(query) if results: @@ -120,90 +123,110 @@ async def search(ctx, *, query: str): @bot.command() -async def crypto(ctx, symbol: str): +async def crypto(ctx: commands, symbol: str): """Get the price of a cryptocurrency using in USD.""" - reply = s.crypto_reply(symbol) - if reply: - await ctx.send(reply) - else: - await ctx.send("Crypto Symbol could not be found.") + await ctx.send( + "Crypto now has native support. Any crypto can be called using two dollar signs: `$$eth` `$$btc` `$$doge`" + ) @bot.command() -async def intra(ctx, sym: str): +async def intra(ctx: commands, sym: str): """Get a chart for the stocks movement since market open.""" - with ctx.channel.typing(): + symbols = s.find_symbols(sym) - symbol = s.find_symbols(sym)[0] + if len(symbols): + symbol = symbols[0] + else: + await ctx.send("No symbols or coins found.") + return - df = s.intra_reply(symbol) - if df.empty: - await ctx.send("Invalid symbol please see `/help` for usage details.") - return + df = s.intra_reply(symbol) + if df.empty: + await ctx.send("Invalid symbol please see `/help` for usage details.") + return + with ctx.channel.typing(): buf = io.BytesIO() mpf.plot( df, type="renko", - title=f"\n${symbol.upper()}", - volume=True, + title=f"\n{symbol.name}", + volume="volume" in df.keys(), style="yahoo", - mav=20, savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), ) - buf.seek(0) - caption = ( - f"\nIntraday chart for ${symbol.upper()} from {df.first_valid_index().strftime('%I:%M')} to" - + f" {df.last_valid_index().strftime('%I:%M')} ET on" - + f" {datetime.date.today().strftime('%d, %b %Y')}" - ) + buf.seek(0) + # Get price so theres no request lag after the image is sent + price_reply = s.price_reply([symbol])[0] await ctx.send( - content=caption, file=discord.File( buf, - filename=f"{symbol.upper()}:{datetime.date.today().strftime('%d%b%Y')}.png", + filename=f"{symbol.name}:intra{datetime.date.today().strftime('%S%M%d%b%Y')}.png", ), + content=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to" + + f" {df.last_valid_index().strftime('%d %b at %H:%M')}", ) - await ctx.send(f"{s.price_reply([symbol])[symbol]}") + await ctx.send(price_reply) @bot.command() -async def chart(ctx, sym: str): - """Get a chart for the stocks movement for the past month.""" - with ctx.channel.typing(): +async def chart(ctx: commands, sym: str): + """returns a chart of the past month of data for a symbol""" - symbol = s.find_symbols(sym)[0] + symbols = s.find_symbols(sym) + + if len(symbols): + symbol = symbols[0] + else: + await ctx.send("No symbols or coins found.") + return + + df = s.chart_reply(symbol) + if df.empty: + await ctx.send("Invalid symbol please see `/help` for usage details.") + return + with ctx.channel.typing(): - df = s.intra_reply(symbol) - if df.empty: - await ctx.send("Invalid symbol please see `/help` for usage details.") - return buf = io.BytesIO() mpf.plot( df, type="candle", - title=f"\n${symbol.upper()}", - volume=True, + title=f"\n{symbol.name}", + volume="volume" in df.keys(), style="yahoo", savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), ) buf.seek(0) - caption = ( - f"\n1 Month chart for ${symbol.upper()} from {df.first_valid_index().strftime('%d, %b %Y')}" - + f" to {df.last_valid_index().strftime('%d, %b %Y')}" - ) - + # Get price so theres no request lag after the image is sent + price_reply = s.price_reply([symbol])[0] await ctx.send( - content=caption, file=discord.File( buf, - filename=f"{symbol.upper()}:{datetime.date.today().strftime('1M%d%b%Y')}.png", + filename=f"{symbol.name}:1M{datetime.date.today().strftime('%d%b%Y')}.png", ), + content=f"\n1 Month chart for {symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}" + + f" to {df.last_valid_index().strftime('%d, %b %Y')}", ) - await ctx.send(f"{s.price_reply([symbol])[symbol]}") + await ctx.send(price_reply) + + +@bot.command() +async def cap(ctx: commands, sym: str): + symbols = s.find_symbols(sym) + if symbols: + with ctx.channel.typing(): + for reply in s.cap_reply(symbols): + await ctx.send(reply) + + +@bot.command() +async def trending(ctx: commands): + with ctx.channel.typing(): + await ctx.send(s.trending()) @bot.event @@ -211,18 +234,18 @@ async def on_message(message): if message.author.id == bot.user.id: return + if message.content: + if message.content[0] == "/": + await bot.process_commands(message) + return - if message.content[0] == "/": - await bot.process_commands(message) - return - - if "$" in message.content: - symbols = s.find_symbols(message.content) + if "$" in message.content: + symbols = s.find_symbols(message.content) - if symbols: - for reply in s.price_reply(symbols).items(): - await message.channel.send(reply[1]) - return + if symbols: + for reply in s.price_reply(symbols): + await message.channel.send(reply) + return bot.run(DISCORD_TOKEN) -- 2.51.2 From 3b10f6eede8fecb75fe484363c7a1a4fd5a507de Mon Sep 17 00:00:00 2001 From: Anson Date: Thu, 5 Aug 2021 01:29:19 -0700 Subject: [PATCH 63/75] add ci --- .../simple-discord-stock-bot/.gitlab-ci.yml | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml b/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml new file mode 100644 index 000000000..0dc89413b --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml @@ -0,0 +1,96 @@ +#Following instructions (as of 2020-04-01): https://docs.gitlab.com/ee/ci/docker/using_kaniko.html +#Kaniko docs are here: https://github.com/GoogleContainerTools/kaniko +#While this example shows building to multiple registries for all branches, with a few modifications +# it can be used to build non-master branches to a "dev" container registry and only build master to +# a production container registry + +image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + +variables: + #More Information on Kaniko Caching: https://cloud.google.com/build/docs/kaniko-cache + KANIKO_CACHE_ARGS: "--cache=true --cache-copy-layers=true --cache-ttl=24h" + VERSIONLABELMETHOD: "OnlyIfThisCommitHasVersion" # options: "OnlyIfThisCommitHasVersion","LastVersionTagInGit" + IMAGE_LABELS: > + --label org.opencontainers.image.vendor=$CI_SERVER_URL/$GITLAB_USER_LOGIN + --label org.opencontainers.image.authors=$CI_SERVER_URL/$GITLAB_USER_LOGIN + --label org.opencontainers.image.revision=$CI_COMMIT_SHA + --label org.opencontainers.image.source=$CI_PROJECT_URL + --label org.opencontainers.image.documentation=$CI_PROJECT_URL + --label org.opencontainers.image.licenses=$CI_PROJECT_URL + --label org.opencontainers.image.url=$CI_PROJECT_URL + --label vcs-url=$CI_PROJECT_URL + --label com.gitlab.ci.user=$CI_SERVER_URL/$GITLAB_USER_LOGIN + --label com.gitlab.ci.email=$GITLAB_USER_EMAIL + --label com.gitlab.ci.tagorbranch=$CI_COMMIT_REF_NAME + --label com.gitlab.ci.pipelineurl=$CI_PIPELINE_URL + --label com.gitlab.ci.commiturl=$CI_PROJECT_URL/commit/$CI_COMMIT_SHA + --label com.gitlab.ci.cijoburl=$CI_JOB_URL + --label com.gitlab.ci.mrurl=$CI_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_ID + +get-latest-git-version: + stage: .pre + image: + name: alpine/git + entrypoint: [""] + rules: + - if: '$VERSIONLABELMETHOD == "LastVersionTagInGit"' + script: + - | + echo "the google kaniko container does not have git and does not have a packge manager to install it" + git clone https://github.com/GoogleContainerTools/kaniko.git + cd kaniko + echo "$(git describe --abbrev=0 --tags)" > ../VERSIONTAG.txt + echo "VERSIONTAG.txt contains $(cat ../VERSIONTAG.txt)" + artifacts: + paths: + - VERSIONTAG.txt + + +.build_with_kaniko: + #Hidden job to use as an "extends" template + stage: build + script: + - | + echo "Building and shipping image to $CI_REGISTRY_IMAGE" + #Build date for opencontainers + BUILDDATE="'$(date '+%FT%T%z' | sed -E -n 's/(\+[0-9]{2})([0-9]{2})$/\1:\2/p')'" #rfc 3339 date + IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.created=$BUILDDATE --label build-date=$BUILDDATE" + #Description for opencontainers + BUILDTITLE=$(echo $CI_PROJECT_TITLE | tr " " "_") + IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.title=$BUILDTITLE --label org.opencontainers.image.description=$BUILDTITLE" + #Add ref.name for opencontainers + IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.ref.name=$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" + + #Build Version Label and Tag from git tag, LastVersionTagInGit was placed by a previous job artifact + if [[ "$VERSIONLABELMETHOD" == "LastVersionTagInGit" ]]; then VERSIONLABEL=$(cat VERSIONTAG.txt); fi + if [[ "$VERSIONLABELMETHOD" == "OnlyIfThisCommitHasVersion" ]]; then VERSIONLABEL=$CI_COMMIT_TAG; fi + if [[ ! -z "$VERSIONLABEL" ]]; then + IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.version=$VERSIONLABEL" + ADDITIONALTAGLIST="$ADDITIONALTAGLIST $VERSIONLABEL" + fi + + ADDITIONALTAGLIST="$ADDITIONALTAGLIST $CI_COMMIT_REF_NAME $CI_COMMIT_SHORT_SHA" + if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then ADDITIONALTAGLIST="$ADDITIONALTAGLIST latest"; fi + if [[ -n "$ADDITIONALTAGLIST" ]]; then + for TAG in $ADDITIONALTAGLIST; do + FORMATTEDTAGLIST="${FORMATTEDTAGLIST} --tag $CI_REGISTRY_IMAGE:$TAG "; + done; + fi + + #Reformat Docker tags to kaniko's --destination argument: + FORMATTEDTAGLIST=$(echo "${FORMATTEDTAGLIST}" | sed s/\-\-tag/\-\-destination/g) + + echo "Kaniko arguments to run: --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile $KANIKO_CACHE_ARGS $FORMATTEDTAGLIST $IMAGE_LABELS" + mkdir -p /kaniko/.docker + echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > /kaniko/.docker/config.json + /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile $KANIKO_CACHE_ARGS $FORMATTEDTAGLIST $IMAGE_LABELS + + +build-for-gitlab-project-registry: + extends: .build_with_kaniko + environment: + #This is only here for completeness, since there are no CI CD Variables with this scope, the project defaults are used + # to push to this projects docker registry + name: push-to-gitlab-project-registry -- 2.51.2 From abe1439ccb4d433cf3475beefe137beda0073630 Mon Sep 17 00:00:00 2001 From: Anson Date: Thu, 5 Aug 2021 01:31:34 -0700 Subject: [PATCH 64/75] added ci --- simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml b/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml index 0dc89413b..1f5ae5c04 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml +++ b/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml @@ -93,4 +93,4 @@ build-for-gitlab-project-registry: environment: #This is only here for completeness, since there are no CI CD Variables with this scope, the project defaults are used # to push to this projects docker registry - name: push-to-gitlab-project-registry + name: push-to-gitlab-project-registry \ No newline at end of file -- 2.51.2 From af4aacd5e7f02f4959f334b8cb15ab9e90422bb1 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 5 Aug 2021 08:33:49 +0000 Subject: [PATCH 65/75] Update .gitlab-ci.yml file -- 2.51.2 From bd705d3321c2e179c49ee144afbacc625ebe2538 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 5 Aug 2021 08:33:52 +0000 Subject: [PATCH 66/75] Update .gitlab-ci.yml file -- 2.51.2 From a8e5b9bab576cdf7a14a435b7a2a1b8519cf07b1 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 15 Aug 2021 17:12:45 -0700 Subject: [PATCH 67/75] added logging and small fixes --- .../simple-discord-stock-bot/bot.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 0d596da19..0b9cb4c8d 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -1,24 +1,15 @@ import datetime -import html import io -import json import logging import os -import random -import string -import traceback -from logging import critical, debug, error, info, warning -from uuid import uuid4 - -import mplfinance as mpf - import discord +import mplfinance as mpf from discord.ext import commands +from discord.flags import Intents -from symbol_router import Router from D_info import D_info - +from symbol_router import Router DISCORD_TOKEN = os.environ["DISCORD"] @@ -30,28 +21,31 @@ client = discord.Client() bot = commands.Bot( - command_prefix="/", - description=d.help_text, + command_prefix="/", description=d.help_text, intents=Intents.default() +) + +logger = logging.getLogger("discord") +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) @bot.event async def on_ready(): - print("Starting Simple Stock Bot") - print("Logged in as") - print(bot.user.name) - print(bot.user.id) - print("------") + logging.info("Starting Simple Stock Bot") + logging.info(f"Logged in as {bot.user.name} {bot.user.id}") @bot.command() async def status(ctx: commands): """Debug command for diagnosing if the bot is experiencing any issues.""" + logging.warning(f"Status command ran by {ctx.message.author}") message = "" try: message = "Contact MisterBiggs#0465 if you need help.\n" message += s.status("") + "\n" except Exception as ex: + logging.critical(ex) message += ( f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n" + "*The bot encountered an error while attempting to find errors. Please contact the bot admin.*" @@ -216,6 +210,7 @@ async def chart(ctx: commands, sym: str): @bot.command() async def cap(ctx: commands, sym: str): + """Get the market cap of a symbol""" symbols = s.find_symbols(sym) if symbols: with ctx.channel.typing(): @@ -225,6 +220,7 @@ async def cap(ctx: commands, sym: str): @bot.command() async def trending(ctx: commands): + """Get a list of Trending Stocks and Coins""" with ctx.channel.typing(): await ctx.send(s.trending()) -- 2.51.2 From b8f8b47d6ab06d1f663d54f13f2015f432aa3f56 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 15 Aug 2021 17:38:53 -0700 Subject: [PATCH 68/75] /status now returns bot latency --- simple-stock-bots/simple-discord-stock-bot/bot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 0b9cb4c8d..fd6d10686 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -43,7 +43,10 @@ async def status(ctx: commands): message = "" try: message = "Contact MisterBiggs#0465 if you need help.\n" - message += s.status("") + "\n" + message += ( + s.status(f"Bot recieved your message in: {bot.latency*1000:.4f}ms") + "\n" + ) + except Exception as ex: logging.critical(ex) message += ( -- 2.51.2 From 0d04b7175001546c61e109c1c9c7cf62530d8e5a Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 15 Aug 2021 17:39:20 -0700 Subject: [PATCH 69/75] further deprecate /crypto command --- simple-stock-bots/simple-discord-stock-bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index fd6d10686..2a3716820 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -120,7 +120,7 @@ async def search(ctx: commands, *, query: str): @bot.command() -async def crypto(ctx: commands, symbol: str): +async def crypto(ctx: commands, _: str): """Get the price of a cryptocurrency using in USD.""" await ctx.send( "Crypto now has native support. Any crypto can be called using two dollar signs: `$$eth` `$$btc` `$$doge`" -- 2.51.2 From 1c449094b42aa413615798ecf94fd923c09cbe85 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 15 Aug 2021 17:48:50 -0700 Subject: [PATCH 70/75] embracing the devcontainers --- .../.devcontainer/devcontainer.json | 25 +++++++++++++++++++ .../simple-discord-stock-bot/.gitignore | 4 +-- .../.vscode/settings.json | 20 +++++++++++++++ .../simple-discord-stock-bot/DockerDev | 7 ++++++ 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json create mode 100644 simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json create mode 100644 simple-stock-bots/simple-discord-stock-bot/DockerDev diff --git a/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json new file mode 100644 index 000000000..1587f4314 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.191.0/containers/docker-existing-dockerfile +{ + "name": "DockerDev", + // Sets the run context to one level up instead of the .devcontainer folder. + "context": "..", + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. + "dockerFile": "../DockerDev", + // Set *default* container specific settings.json values on container create. + "settings": {}, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python" + ] + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "apt-get update && apt-get install -y curl", + // Uncomment when using a ptrace-based debugger like C++, Go, and Rust + // "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], + // Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker. + // "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitignore b/simple-stock-bots/simple-discord-stock-bot/.gitignore index 5e5820c48..ed8ebf583 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.gitignore +++ b/simple-stock-bots/simple-discord-stock-bot/.gitignore @@ -1,3 +1 @@ -.vscode/settings.json -__pycache__ -.devcontainer \ No newline at end of file +__pycache__ \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json b/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json new file mode 100644 index 000000000..90f899ac1 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "workbench.iconTheme": "vscode-icons", + "editor.suggestSelection": "first", + "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", + "python.languageServer": "Pylance", + "git.autofetch": true, + "editor.formatOnSave": true, + "files.associations": { + "DockerDev": "dockerfile", + }, + "workbench.colorTheme": "One Dark Pro", + "explorer.confirmDelete": false, + "editor.fontFamily": "JetBrains Mono", + "editor.letterSpacing": 1.2, + "editor.fontLigatures": true, + "editor.wordWrap": "on", + "python.formatting.provider": "black", + "python.showStartPage": false, + "editor.fontSize": 13, +} \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/DockerDev b/simple-stock-bots/simple-discord-stock-bot/DockerDev new file mode 100644 index 000000000..ab818c3d0 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/DockerDev @@ -0,0 +1,7 @@ +FROM python:3.9-buster + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir black +COPY . . + -- 2.51.2 From a5c8b9b2c5c948cfe732e0cce6f70f741c02887d Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 15 Aug 2021 19:35:31 -0700 Subject: [PATCH 71/75] parity with telegram merge request 26 --- .../simple-discord-stock-bot/IEX_Symbol.py | 151 +++++++---------- .../simple-discord-stock-bot/cg_Crypto.py | 160 ++++++++++-------- 2 files changed, 154 insertions(+), 157 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py index 87d16f3b7..3eff964ea 100644 --- a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py +++ b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py @@ -1,6 +1,7 @@ """Class with functions for running the bot with IEX Cloud. """ +import logging import os from datetime import datetime from logging import warning @@ -47,6 +48,30 @@ class IEX_Symbol: schedule.every().day.do(self.get_symbol_list) schedule.every().day.do(self.clear_charts) + def get(self, endpoint, params: dict = {}, timeout=5) -> dict: + + url = "https://cloud.iexapis.com/stable" + endpoint + + # set token param if it wasn't passed. + params["token"] = params.get("token", self.IEX_TOKEN) + + resp = r.get(url, params=params, timeout=timeout) + + # Make sure API returned a proper status code + try: + resp.raise_for_status() + except r.exceptions.HTTPError as e: + logging.error(e) + return {} + + # Make sure API returned valid JSON + try: + resp_json = resp.json() + return resp_json + except r.exceptions.JSONDecodeError as e: + logging.error(e) + return {} + def clear_charts(self) -> None: """ Clears cache of chart data. @@ -71,14 +96,8 @@ class IEX_Symbol: If `return_df` is set to `True` returns a dataframe, otherwise returns `None`. """ - reg_symbols = r.get( - f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}", - timeout=5, - ).json() - otc_symbols = r.get( - f"https://cloud.iexapis.com/stable/ref-data/otc/symbols?token={self.IEX_TOKEN}", - timeout=5, - ).json() + reg_symbols = self.get("/ref-data/symbols") + otc_symbols = self.get("/ref-data/otc/symbols") reg = pd.DataFrame(data=reg_symbols) otc = pd.DataFrame(data=otc_symbols) @@ -105,7 +124,7 @@ class IEX_Symbol: """ resp = r.get( "https://pjmps0c34hp7.statuspage.io/api/v2/status.json", - timeout=5, + timeout=15, ) if resp.status_code == 200: @@ -174,14 +193,7 @@ class IEX_Symbol: Formatted markdown """ - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" - - response = r.get( - IEXurl, - timeout=5, - ) - if response.status_code == 200: - IEXData = response.json() + if IEXData := self.get(f"/stock/{symbol.id}/quote"): if symbol.symbol.upper() in self.otc_list: return f"OTC - {symbol.symbol.upper()}, {IEXData['companyName']} most recent price is: $**{IEXData['latestPrice']}**" @@ -249,13 +261,11 @@ class IEX_Symbol: if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" - response = r.get( - IEXurl, - timeout=5, - ) - if response.status_code == 200 and response.json(): - IEXData = response.json()[0] + if resp := self.get(f"/stock/{symbol.id}/dividends/next"): + try: + IEXData = resp[0] + except IndexError as e: + return f"${symbol.id.upper()} either doesn't exist or pays no dividend." keys = ( "amount", "currency", @@ -313,28 +323,19 @@ class IEX_Symbol: if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/15?token={self.IEX_TOKEN}" - response = r.get( - IEXurl, - timeout=5, - ) - if response.status_code == 200: - data = response.json() - if data: - line = [] + if data := self.get(f"/stock/{symbol.id}/news/last/15"): + line = [] + + for news in data: + if news["lang"] == "en" and not news["hasPaywall"]: + line.append( + f"*{news['source']}*: [{news['headline']}]({news['url']})" + ) - for news in data: - if news["lang"] == "en" and not news["hasPaywall"]: - line.append( - f"*{news['source']}*: [{news['headline']}]({news['url']})" - ) + return f"News for **{symbol.id.upper()}**:\n" + "\n".join(line[:5]) - else: - return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." else: - return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." - - return f"News for **{symbol.id.upper()}**:\n" + "\n".join(line[:5]) + return f"No news found for: {symbol.id}\nEither today is boring or the symbol does not exist." def info_reply(self, symbol: Stock) -> str: """Gets description for Stock @@ -351,14 +352,7 @@ class IEX_Symbol: if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" - response = r.get( - IEXurl, - timeout=5, - ) - - if response.status_code == 200: - data = response.json() + if data := self.get(f"/stock/{symbol.id}/company"): [data.pop(k) for k in list(data) if data[k] == ""] if "description" in data: @@ -381,14 +375,7 @@ class IEX_Symbol: if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" - response = r.get( - IEXurl, - timeout=5, - ) - - if response.status_code == 200: - data = response.json() + if data := self.get(f"/stock/{symbol.id}/stats"): [data.pop(k) for k in list(data) if data[k] == ""] m = "" @@ -412,25 +399,20 @@ class IEX_Symbol: else: return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - def cap_reply(self, stock: Stock) -> str: + def cap_reply(self, symbol: Stock) -> str: """Get the Market Cap of a stock""" - response = r.get( - f"https://cloud.iexapis.com/stable/stock/{stock.id}/stats?token={self.IEX_TOKEN}", - timeout=5, - ) - if response.status_code == 200: - try: - data = response.json() + if data := self.get(f"/stable/stock/{symbol.id}/stats"): + try: cap = data["marketcap"] except KeyError: - return f"{stock.id} returned an error." + return f"{symbol.id} returned an error." - message = f"The current market cap of {stock.name} is $**{cap:,.2f}**" + message = f"The current market cap of {symbol.name} is $**{cap:,.2f}**" else: - message = f"The Coin: {stock.name} was not found or returned and error." + message = f"The Stock: {symbol.name} was not found or returned and error." return message @@ -453,13 +435,8 @@ class IEX_Symbol: if symbol.id.upper() not in list(self.symbol_list["symbol"]): return pd.DataFrame() - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}" - response = r.get( - IEXurl, - timeout=5, - ) - if response.status_code == 200: - df = pd.DataFrame(response.json()) + if data := self.get(f"/stock/{symbol.id}/intraday-prices"): + df = pd.DataFrame(data) df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) df = df.set_index("DT") @@ -494,13 +471,11 @@ class IEX_Symbol: except KeyError: pass - response = r.get( - f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false", - timeout=5, - ) - - if response.status_code == 200: - df = pd.DataFrame(response.json()) + if data := self.get( + f"/stock/{symbol.id}/chart/1mm", + params={"chartInterval": 3, "includeToday": "false"}, + ): + df = pd.DataFrame(data) df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) df = df.set_index("DT") @@ -518,14 +493,10 @@ class IEX_Symbol: list of $ID: NAME, CHANGE% """ - stocks = r.get( - f"https://cloud.iexapis.com/stable/stock/market/list/mostactive?token={self.IEX_TOKEN}", - timeout=5, - ) - if stocks.status_code == 200: + if data := self.get(f"/stock/market/list/mostactive"): return [ f"`${s['symbol']}`: {s['companyName']}, {100*s['changePercent']:.2f}%" - for s in stocks.json() + for s in data ] else: return ["Trending Stocks Currently Unavailable."] diff --git a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py index 00f248c96..84944b298 100644 --- a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py +++ b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py @@ -1,6 +1,7 @@ """Class with functions for running the bot with IEX Cloud. """ +import logging from datetime import datetime from typing import List, Optional, Tuple @@ -33,6 +34,25 @@ class cg_Crypto: self.get_symbol_list() schedule.every().day.do(self.get_symbol_list) + def get(self, endpoint, params: dict = {}, timeout=10) -> dict: + + url = "https://api.coingecko.com/api/v3" + endpoint + resp = r.get(url, params=params, timeout=timeout) + # Make sure API returned a proper status code + try: + resp.raise_for_status() + except r.exceptions.HTTPError as e: + logging.error(e) + return {} + + # Make sure API returned valid JSON + try: + resp_json = resp.json() + return resp_json + except r.exceptions.JSONDecodeError as e: + logging.error(e) + return {} + def symbol_id(self, symbol) -> str: try: return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[ @@ -45,10 +65,7 @@ class cg_Crypto: self, return_df=False ) -> Optional[Tuple[pd.DataFrame, datetime]]: - raw_symbols = r.get( - "https://api.coingecko.com/api/v3/coins/list", - timeout=5, - ).json() + raw_symbols = self.get("/coins/list") symbols = pd.DataFrame(data=raw_symbols) symbols["description"] = ( @@ -69,15 +86,16 @@ class cg_Crypto: str Human readable text on status of CoinGecko API """ - status = r.get( - "https://api.coingecko.com/api/v3/ping", + status = self.get( + "/ping", timeout=5, ) - if status.status_code == 200: - return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." - else: - return f"CoinGecko API returned an error in {status.elapsed.total_seconds()} Seconds." + try: + status.raise_for_status() + return f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} Seconds." + except: + return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." def search_symbols(self, search: str) -> List[Tuple[str, str]]: """Performs a fuzzy search to find coin symbols closest to a search term. @@ -133,14 +151,16 @@ class cg_Crypto: markdown formatted string of the symbols price and movement. """ - response = r.get( - f"https://api.coingecko.com/api/v3/simple/price?ids={coin.id}&vs_currencies={self.vs_currency}&include_24hr_change=true", - timeout=5, - ) - if response.status_code == 200: - + if resp := self.get( + "/simple/price", + params={ + "ids": coin.id, + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + ): try: - data = response.json()[coin.id] + data = resp[coin.id] price = data[self.vs_currency] change = data[self.vs_currency + "_24h_change"] @@ -160,7 +180,7 @@ class cg_Crypto: message += ", the coin hasn't shown any movement today." else: - message = f"The Coin: {coin.name} was not found." + message = f"The price for {coin.name} is not available. If you suspect this is an error run `/status`" return message @@ -177,13 +197,13 @@ class cg_Crypto: pd.DataFrame Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ - response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1", - timeout=5, - ) - if response.status_code == 200: + + if resp := self.get( + f"/coins/{symbol.id}/ohlc", + params={"vs_currency": self.vs_currency, "days": 1}, + ): df = pd.DataFrame( - response.json(), columns=["Date", "Open", "High", "Low", "Close"] + resp, columns=["Date", "Open", "High", "Low", "Close"] ).dropna() df["Date"] = pd.to_datetime(df["Date"], unit="ms") df = df.set_index("Date") @@ -205,14 +225,13 @@ class cg_Crypto: pd.DataFrame Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ - response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30", - timeout=5, - ) - if response.status_code == 200: + if resp := self.get( + f"/coins/{symbol.id}/ohlc", + params={"vs_currency": self.vs_currency, "days": 30}, + ): df = pd.DataFrame( - response.json(), columns=["Date", "Open", "High", "Low", "Close"] + resp, columns=["Date", "Open", "High", "Low", "Close"] ).dropna() df["Date"] = pd.to_datetime(df["Date"], unit="ms") df = df.set_index("Date") @@ -233,12 +252,12 @@ class cg_Crypto: Preformatted markdown. """ - response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", - timeout=5, - ) - if response.status_code == 200: - data = response.json() + if data := self.get( + f"/coins/{symbol.id}", + params={ + "localization": "false", + }, + ): return f""" [{data['name']}]({data['links']['homepage'][0]}) Statistics: @@ -265,14 +284,18 @@ class cg_Crypto: str Preformatted markdown. """ - response = r.get( - f"https://api.coingecko.com/api/v3/simple/price?ids={coin.id}&vs_currencies={self.vs_currency}&include_market_cap=true", - timeout=5, - ) - if response.status_code == 200: + if resp := self.get( + f"/simple/price", + params={ + "ids": coin.id, + "vs_currencies": self.vs_currency, + "include_market_cap": "true", + }, + ): + print(resp) try: - data = response.json()[coin.id] + data = resp[coin.id] price = data[self.vs_currency] cap = data[self.vs_currency + "_market_cap"] @@ -302,12 +325,10 @@ class cg_Crypto: Preformatted markdown. """ - response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", - timeout=5, - ) - if response.status_code == 200: - data = response.json() + if data := self.get( + f"/coins/{symbol.id}", + params={"localization": "false"}, + ): try: return markdownify(data["description"]["en"]) except KeyError: @@ -324,28 +345,29 @@ class cg_Crypto: list of $$ID: NAME, CHANGE% """ - coins = r.get( - "https://api.coingecko.com/api/v3/search/trending", - timeout=5, - ) + coins = self.get("/search/trending") try: trending = [] - if coins.status_code == 200: - for coin in coins.json()["coins"]: - c = coin["item"] + for coin in coins["coins"]: + c = coin["item"] - sym = c["symbol"].upper() - name = c["name"] - change = r.get( - f"https://api.coingecko.com/api/v3/simple/price?ids={c['id']}&vs_currencies={self.vs_currency}&include_24hr_change=true" - ).json()[c["id"]]["usd_24h_change"] + sym = c["symbol"].upper() + name = c["name"] + change = self.get( + f"/simple/price", + params={ + "ids": c["id"], + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + )[c["id"]]["usd_24h_change"] - msg = f"`$${sym}`: {name}, {change:.2f}%" + msg = f"`$${sym}`: {name}, {change:.2f}%" - trending.append(msg) + trending.append(msg) except Exception as e: - print(e) + logging.warning(e) trending = ["Trending Coins Currently Unavailable."] return trending @@ -364,10 +386,14 @@ class cg_Crypto: """ query = ",".join([c.id for c in coins]) - prices = r.get( - f"https://api.coingecko.com/api/v3/simple/price?ids={query}&vs_currencies=usd&include_24hr_change=true", - timeout=5, - ).json() + prices = self.get( + f"/simple/price", + params={ + "ids": query, + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + ) replies = [] for coin in coins: -- 2.51.2 From 0e00259c3dcd7166ec0c5f1128d051d71c398127 Mon Sep 17 00:00:00 2001 From: Anson Date: Sun, 15 Aug 2021 19:51:38 -0700 Subject: [PATCH 72/75] bug fix telegram pr 27 --- simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py index 84944b298..17de1e326 100644 --- a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py +++ b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py @@ -86,8 +86,8 @@ class cg_Crypto: str Human readable text on status of CoinGecko API """ - status = self.get( - "/ping", + status = r.get( + "https://api.coingecko.com/api/v3/ping", timeout=5, ) -- 2.51.2 From e6bec8aecd315da72e607e835e53033bed7dbed2 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 4 Jan 2022 20:54:15 -0700 Subject: [PATCH 73/75] add devcontainer stuff from telegram bot --- .../.devcontainer/Dockerfile | 24 +++++++++++++++++++ .../.devcontainer/devcontainer.json | 5 ++-- .../simple-discord-stock-bot/DockerDev | 7 ------ 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile delete mode 100644 simple-stock-bots/simple-discord-stock-bot/DockerDev diff --git a/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile new file mode 100644 index 000000000..04540b27a --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.9-buster AS builder + + +COPY requirements.txt /requirements.txt +RUN pip install --user -r requirements.txt + + +FROM python:3.9-slim + +ENV MPLBACKEND=Agg + +COPY --from=builder /root/.local /root/.local + +# Formatting +RUN pip install --no-cache-dir black +# Jupyter Notebooks +RUN pip install --no-cache-dir ipykernel + +ENV DISCORD=TOKEN +ENV IEX=TOKEN + +COPY . . + +# CMD [ "python", "./bot.py" ] diff --git a/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json index 1587f4314..de09b464b 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json +++ b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/devcontainer.json @@ -5,12 +5,13 @@ // Sets the run context to one level up instead of the .devcontainer folder. "context": "..", // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. - "dockerFile": "../DockerDev", + "dockerFile": "Dockerfile", // Set *default* container specific settings.json values on container create. "settings": {}, // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "ms-python.python" + "ms-python.python", + "ms-azuretools.vscode-docker" ] // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], diff --git a/simple-stock-bots/simple-discord-stock-bot/DockerDev b/simple-stock-bots/simple-discord-stock-bot/DockerDev deleted file mode 100644 index ab818c3d0..000000000 --- a/simple-stock-bots/simple-discord-stock-bot/DockerDev +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.9-buster - -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir black -COPY . . - -- 2.51.2 From 61027c9b3147bb28397869f4430b86d9cdf9b7ee Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 4 Jan 2022 21:54:28 -0700 Subject: [PATCH 74/75] updated to newest version of discord and bot files --- .../.vscode/launch.json | 15 + .../.vscode/settings.json | 7 - .../simple-discord-stock-bot/Dockerfile | 17 +- .../simple-discord-stock-bot/IEX_Symbol.py | 89 ++- .../simple-discord-stock-bot/Symbol.py | 46 +- .../simple-discord-stock-bot/cg_Crypto.py | 64 +- .../simple-discord-stock-bot/functions.py | 545 ------------------ .../simple-discord-stock-bot/requirements.txt | 7 +- .../simple-discord-stock-bot/symbol_router.py | 198 ++++--- 9 files changed, 239 insertions(+), 749 deletions(-) create mode 100644 simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json delete mode 100644 simple-stock-bots/simple-discord-stock-bot/functions.py diff --git a/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json b/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json new file mode 100644 index 000000000..a49a5a1ae --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Discord Bot", + "type": "python", + "request": "launch", + "program": "bot.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json b/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json index 90f899ac1..2c898aeb4 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json +++ b/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json @@ -8,13 +8,6 @@ "files.associations": { "DockerDev": "dockerfile", }, - "workbench.colorTheme": "One Dark Pro", - "explorer.confirmDelete": false, - "editor.fontFamily": "JetBrains Mono", - "editor.letterSpacing": 1.2, - "editor.fontLigatures": true, - "editor.wordWrap": "on", "python.formatting.provider": "black", "python.showStartPage": false, - "editor.fontSize": 13, } \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile index 2de21c081..7ae49d2fe 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -1,8 +1,17 @@ -FROM python:3.9-buster +FROM python:3.9-buster AS builder + + +COPY requirements.txt /requirements.txt +RUN pip install --user -r requirements.txt + + +FROM python:3.9-slim + +ENV MPLBACKEND=Agg + +COPY --from=builder /root/.local /root/.local -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD [ "python", "./bot.py" ] +CMD [ "python", "./bot.py" ] \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py index 3eff964ea..6b933593d 100644 --- a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py +++ b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py @@ -10,7 +10,6 @@ from typing import List, Optional, Tuple import pandas as pd import requests as r import schedule -from fuzzywuzzy import fuzz from Symbol import Stock @@ -25,6 +24,7 @@ class IEX_Symbol: searched_symbols = {} otc_list = [] charts = {} + trending_cache = None def __init__(self) -> None: """Creates a Symbol Object @@ -36,6 +36,9 @@ class IEX_Symbol: """ try: self.IEX_TOKEN = os.environ["IEX"] + + if self.IEX_TOKEN == "TOKEN": + self.IEX_TOKEN = "" except KeyError: self.IEX_TOKEN = "" warning( @@ -67,6 +70,13 @@ class IEX_Symbol: # Make sure API returned valid JSON try: resp_json = resp.json() + + # IEX uses backtick ` as apostrophe which breaks telegram markdown parsing + if type(resp_json) is dict: + resp_json["companyName"] = resp_json.get("companyName", "").replace( + "`", "'" + ) + return resp_json except r.exceptions.JSONDecodeError as e: logging.error(e) @@ -105,6 +115,9 @@ class IEX_Symbol: symbols = pd.concat([reg, otc]) + # IEX uses backtick ` as apostrophe which breaks telegram markdown parsing + symbols["name"] = symbols["name"].str.replace("`", "'") + symbols["description"] = "$" + symbols["symbol"] + ": " + symbols["name"] symbols["id"] = symbols["symbol"] symbols["type_id"] = "$" + symbols["symbol"].str.lower() @@ -122,6 +135,10 @@ class IEX_Symbol: str Human readable text on status of IEX API """ + + if self.IEX_TOKEN == "": + return "The `IEX_TOKEN` is not set so Stock Market data is not available." + resp = r.get( "https://pjmps0c34hp7.statuspage.io/api/v2/status.json", timeout=15, @@ -140,46 +157,6 @@ class IEX_Symbol: + " Please check the status page for more information. https://status.iexapis.com" ) - def search_symbols(self, search: str) -> List[Tuple[str, str]]: - """Performs a fuzzy search to find stock symbols closest to a search term. - - Parameters - ---------- - search : str - String used to search, could be a company name or something close to the companies stock ticker. - - Returns - ------- - List[tuple[str, str]] - A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). - """ - - schedule.run_pending() - search = search.lower() - try: # https://stackoverflow.com/a/3845776/8774114 - return self.searched_symbols[search] - except KeyError: - pass - - symbols = self.symbol_list - symbols["Match"] = symbols.apply( - lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), - axis=1, - ) - - symbols.sort_values(by="Match", ascending=False, inplace=True) - if symbols["Match"].head().sum() < 300: - symbols["Match"] = symbols.apply( - lambda x: fuzz.partial_ratio(search, x["name"].lower()), - axis=1, - ) - - symbols.sort_values(by="Match", ascending=False, inplace=True) - symbols = symbols.head(10) - symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) - self.searched_symbols[search] = symbol_list - return symbol_list - def price_reply(self, symbol: Stock) -> str: """Returns price movement of Stock for the last market day, or after hours. @@ -265,7 +242,8 @@ class IEX_Symbol: try: IEXData = resp[0] except IndexError as e: - return f"${symbol.id.upper()} either doesn't exist or pays no dividend." + logging.info(e) + return f"Getting dividend information for ${symbol.id.upper()} encountered an error. The provider for upcoming dividend information has been having issues recently which has likely caused this error. It is also possible that the stock has no dividend or does not exist." keys = ( "amount", "currency", @@ -306,7 +284,7 @@ class IEX_Symbol: + f"\n\nThe dividend was declared on {declared} and the ex-dividend date is {ex}" ) - return f"${symbol.id.upper()} either doesn't exist or pays no dividend." + return f"Getting dividend information for ${symbol.id.upper()} encountered an error. The provider for upcoming dividend information has been having issues recently which has likely caused this error. It is also possible that the stock has no dividend or does not exist." def news_reply(self, symbol: Stock) -> str: """Gets most recent, english, non-paywalled news @@ -402,7 +380,7 @@ class IEX_Symbol: def cap_reply(self, symbol: Stock) -> str: """Get the Market Cap of a stock""" - if data := self.get(f"/stable/stock/{symbol.id}/stats"): + if data := self.get(f"/stock/{symbol.id}/stats"): try: cap = data["marketcap"] @@ -484,6 +462,23 @@ class IEX_Symbol: return pd.DataFrame() + def spark_reply(self, symbol: Stock) -> str: + quote = self.get(f"/stock/{symbol.id}/quote") + + open_change = quote.get("changePercent", 0) + after_change = quote.get("extendedChangePercent", 0) + + change = 0 + + if open_change: + change = change + open_change + if after_change: + change = change + after_change + + change = change * 100 + + return f"`{symbol.tag}`: {quote['companyName']}, {change:.2f}%" + def trending(self) -> list[str]: """Gets current coins trending on IEX. Only returns when market is open. @@ -494,9 +489,9 @@ class IEX_Symbol: """ if data := self.get(f"/stock/market/list/mostactive"): - return [ + self.trending_cache = [ f"`${s['symbol']}`: {s['companyName']}, {100*s['changePercent']:.2f}%" for s in data ] - else: - return ["Trending Stocks Currently Unavailable."] + + return self.trending_cache diff --git a/simple-stock-bots/simple-discord-stock-bot/Symbol.py b/simple-stock-bots/simple-discord-stock-bot/Symbol.py index db04b4a2a..06c25c03d 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Symbol.py +++ b/simple-stock-bots/simple-discord-stock-bot/Symbol.py @@ -1,6 +1,5 @@ -import functools - -import requests as r +import pandas as pd +import logging class Symbol: @@ -8,6 +7,7 @@ class Symbol: symbol: What the user calls it. ie tsla or btc id: What the api expects. ie tsla or bitcoin name: Human readable. ie Tesla or Bitcoin + tag: Uppercase tag to call the symbol. ie $TSLA or $$BTC """ currency = "usd" @@ -17,6 +17,7 @@ class Symbol: self.symbol = symbol self.id = symbol self.name = symbol + self.tag = "$" + symbol def __repr__(self) -> str: return f"<{self.__class__.__name__} instance of {self.id} at {id(self)}>" @@ -28,31 +29,26 @@ class Symbol: class Stock(Symbol): """Stock Market Object. Gets data from IEX Cloud""" - def __init__(self, symbol: str) -> None: - self.symbol = symbol - self.id = symbol - self.name = "$" + symbol.upper() - + def __init__(self, symbol: pd.DataFrame) -> None: + if len(symbol) > 1: + logging.info(f"Crypto with shared id:\n\t{symbol.id}") + symbol = symbol.head(1) -# Used by Coin to change symbols for ids -coins = r.get("https://api.coingecko.com/api/v3/coins/list").json() + self.symbol = symbol.symbol.values[0] + self.id = symbol.id.values[0] + self.name = symbol.name.values[0] + self.tag = symbol.type_id.values[0].upper() class Coin(Symbol): """Cryptocurrency Object. Gets data from CoinGecko.""" - @functools.cache - def __init__(self, symbol: str) -> None: - self.symbol = symbol - self.get_data() - - def get_data(self) -> None: - self.id = list(filter(lambda coin: coin["symbol"] == self.symbol, coins))[0][ - "id" - ] - data = r.get("https://api.coingecko.com/api/v3/coins/" + self.id).json() - self.data = data - - self.name = data["name"] - self.description = data["description"] - # self.price = data["market_data"]["current_price"][self.currency] + def __init__(self, symbol: pd.DataFrame) -> None: + if len(symbol) > 1: + logging.info(f"Crypto with shared id:\n\t{symbol.id}") + symbol = symbol.head(1) + + self.symbol = symbol.symbol.values[0] + self.id = symbol.id.values[0] + self.name = symbol.name.values[0] + self.tag = symbol.type_id.values[0].upper() diff --git a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py index 17de1e326..cc3789441 100644 --- a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py +++ b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py @@ -3,12 +3,12 @@ import logging from datetime import datetime +from logging import critical, debug, error, info, warning from typing import List, Optional, Tuple import pandas as pd import requests as r import schedule -from fuzzywuzzy import fuzz from markdownify import markdownify from Symbol import Coin @@ -22,6 +22,7 @@ class cg_Crypto: vs_currency = "usd" # simple/supported_vs_currencies for list of options searched_symbols = {} + trending_cache = None def __init__(self) -> None: """Creates a Symbol Object @@ -68,11 +69,14 @@ class cg_Crypto: raw_symbols = self.get("/coins/list") symbols = pd.DataFrame(data=raw_symbols) + # Removes all binance-peg symbols + symbols = symbols[~symbols["id"].str.contains("binance-peg")] + symbols["description"] = ( "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"] ) symbols = symbols[["id", "symbol", "name", "description"]] - symbols["type_id"] = "$$" + symbols["id"] + symbols["type_id"] = "$$" + symbols["symbol"] self.symbol_list = symbols if return_df: @@ -97,45 +101,6 @@ class cg_Crypto: except: return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." - def search_symbols(self, search: str) -> List[Tuple[str, str]]: - """Performs a fuzzy search to find coin symbols closest to a search term. - - Parameters - ---------- - search : str - String used to search, could be a company name or something close to the companies coin ticker. - - Returns - ------- - List[tuple[str, str]] - A list tuples of every coin sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). - """ - schedule.run_pending() - search = search.lower() - try: # https://stackoverflow.com/a/3845776/8774114 - return self.searched_symbols[search] - except KeyError: - pass - - symbols = self.symbol_list - symbols["Match"] = symbols.apply( - lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), - axis=1, - ) - - symbols.sort_values(by="Match", ascending=False, inplace=True) - if symbols["Match"].head().sum() < 300: - symbols["Match"] = symbols.apply( - lambda x: fuzz.partial_ratio(search, x["name"].lower()), - axis=1, - ) - - symbols.sort_values(by="Match", ascending=False, inplace=True) - symbols = symbols.head(10) - symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) - self.searched_symbols[search] = symbol_list - return symbol_list - def price_reply(self, coin: Coin) -> str: """Returns current market price or after hours if its available for a given coin symbol. @@ -293,7 +258,7 @@ class cg_Crypto: "include_market_cap": "true", }, ): - print(resp) + debug(resp) try: data = resp[coin.id] @@ -336,6 +301,18 @@ class cg_Crypto: return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + def spark_reply(self, symbol: Coin) -> str: + change = self.get( + f"/simple/price", + params={ + "ids": symbol.id, + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + )[symbol.id]["usd_24h_change"] + + return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%" + def trending(self) -> list[str]: """Gets current coins trending on coingecko @@ -368,8 +345,9 @@ class cg_Crypto: except Exception as e: logging.warning(e) - trending = ["Trending Coins Currently Unavailable."] + return self.trending_cache + self.trending_cache = trending return trending def batch_price(self, coins: list[Coin]) -> list[str]: diff --git a/simple-stock-bots/simple-discord-stock-bot/functions.py b/simple-stock-bots/simple-discord-stock-bot/functions.py deleted file mode 100644 index 0733ba4c0..000000000 --- a/simple-stock-bots/simple-discord-stock-bot/functions.py +++ /dev/null @@ -1,545 +0,0 @@ -import re -from datetime import datetime -from typing import Optional, List, Tuple, Dict - -import pandas as pd -import requests as r -import schedule -from fuzzywuzzy import fuzz - - -class Symbol: - """ - Functions for finding stock market information about symbols. - """ - - SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" - - searched_symbols = {} - charts = {} - - license = re.sub( - r"\b\n", - " ", - r.get( - "https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE" - ).text, - ) - - help_text = """ -Thanks for using this bot, consider supporting it by buying me a beer: https://www.buymeacoffee.com/Anson) - -Keep up with the latest news for the bot in its Discord Server: https://discord.gg/VtEHyyTBAK - -Full documentation on using and running your own stock bot can be found here: https://simple-stock-bots.gitlab.io/site - -**Commands** - - /donate information on how to donate. 🎗️ - - /dividend $[symbol] will return dividend information for the symbol. 📅 - - /intra $[symbol] Plot of the stocks movement since the last market open. 📈 - - /chart $[symbol] Plot of the stocks movement for the past 1 month. 📊 - - /news $[symbol] News about the symbol. 📰 - - /info $[symbol] General information about the symbol. ℹ️ - - /stat $[symbol] Key statistics about the symbol. 🔢 - - /help Get some help using the bot. 🆘 - -**Inline Features** - You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots - full list of stock symbols and return the price of the ticker. Then once you select the ticker - want the bot will send a message as you in that chat with the latest stock price. - The bot also looks at every message in any chat it is in for stock symbols.Symbols start with a - `$` followed by the stock symbol. For example:$tsla would return price information for Tesla Motors. - Market data is provided by [IEX Cloud](https://iexcloud.io) - - If you believe the bot is not behaving properly run `/status`. - """ - - donate_text = """ -Simple Stock Bot is run entirely on donations. -All donations go directly towards paying for servers, and market data is provided by -[IEX Cloud](https://iexcloud.io/). - -The best way to donate is through https://www.buymeacoffee.com/Anson which accepts Paypal or Credit card. -If you have any questions get in touch: MisterBiggs#0465 or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/) - - """ - - def __init__(self, IEX_TOKEN: str) -> None: - """Creates a Symbol Object - - Parameters - ---------- - IEX_TOKEN : str - IEX Token - """ - self.IEX_TOKEN = IEX_TOKEN - if IEX_TOKEN != "": - self.get_symbol_list() - - schedule.every().day.do(self.get_symbol_list) - schedule.every().day.do(self.clear_charts) - - def clear_charts(self) -> None: - """Clears cache of chart data.""" - self.charts = {} - - def get_symbol_list(self, return_df=False) -> Optional[pd.DataFrame]: - - raw_symbols = r.get( - f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" - ).json() - symbols = pd.DataFrame(data=raw_symbols) - - symbols["description"] = symbols["symbol"] + ": " + symbols["name"] - self.symbol_list = symbols - if return_df: - return symbols, datetime.now() - - def iex_status(self) -> str: - """Checks IEX Status dashboard for any current API issues. - - Returns - ------- - str - Human readable text on status of IEX API - """ - status = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json").json()[ - "status" - ] - - if status["indicator"] == "none": - return "IEX Cloud is currently not reporting any issues with its API." - else: - return ( - f"{status['indicator']}: {status['description']}." - + " Please check the status page for more information. https://status.iexapis.com" - ) - - def message_status(self) -> str: - """Checks to see if the bot has available IEX Credits - - Returns - ------- - str - Human readable text on status of IEX Credits. - """ - usage = r.get( - f"https://cloud.iexapis.com/stable/account/metadata?token={self.IEX_TOKEN}" - ).json() - try: - if ( - usage["messagesUsed"] >= usage["messageLimit"] - 10000 - and not usage["payAsYouGoEnabled"] - ): - return "Bot may be out of IEX Credits." - else: - return "Bot has available IEX Credits." - except KeyError: - return "**IEX API could not be reached.**" - - def search_symbols(self, search: str) -> List[Tuple[str, str]]: - """Performs a fuzzy search to find stock symbols closest to a search term. - - Parameters - ---------- - search : str - String used to search, could be a company name or something close to the companies stock ticker. - - Returns - ------- - List[tuple[str, str]] - A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). - """ - - schedule.run_pending() - search = search.lower() - try: # https://stackoverflow.com/a/3845776/8774114 - return self.searched_symbols[search] - except KeyError: - pass - - symbols = self.symbol_list - symbols["Match"] = symbols.apply( - lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), - axis=1, - ) - - symbols.sort_values(by="Match", ascending=False, inplace=True) - if symbols["Match"].head().sum() < 300: - symbols["Match"] = symbols.apply( - lambda x: fuzz.partial_ratio(search, x["name"].lower()), - axis=1, - ) - - symbols.sort_values(by="Match", ascending=False, inplace=True) - symbols = symbols.head(10) - symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) - self.searched_symbols[search] = symbol_list - return symbol_list - - def find_symbols(self, text: str) -> List[str]: - """Finds stock tickers starting with a dollar sign in a blob of text and returns them in a list. - Only returns each match once. Example: Whats the price of $tsla? - - Parameters - ---------- - text : str - Blob of text. - - Returns - ------- - List[str] - List of stock symbols as strings without dollar sign. - """ - - return list(set(re.findall(self.SYMBOL_REGEX, text))) - - def price_reply(self, symbols: list) -> Dict[str, str]: - """Returns current market price or after hours if its available for a given stock symbol. - - Parameters - ---------- - symbols : list - List of stock symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable - markdown formatted string of the symbols price and movement. - """ - dataMessages = {} - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={self.IEX_TOKEN}" - - response = r.get(IEXurl) - if response.status_code == 200: - IEXData = response.json() - keys = ( - "isUSMarketOpen", - "extendedChangePercent", - "extendedPrice", - "companyName", - "latestPrice", - "changePercent", - ) - - if set(keys).issubset(IEXData): - - try: # Some symbols dont return if the market is open - IEXData["isUSMarketOpen"] - except KeyError: - IEXData["isUSMarketOpen"] = True - - if ( - IEXData["isUSMarketOpen"] - or (IEXData["extendedChangePercent"] is None) - or (IEXData["extendedPrice"] is None) - ): # Check if market is open. - message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" - change = round(IEXData["changePercent"] * 100, 2) - else: - message = ( - f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}**," - + f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**" - ) - change = round(IEXData["extendedChangePercent"] * 100, 2) - - # Determine wording of change text - if change > 0: - message += f", the stock is currently **up {change}%**" - elif change < 0: - message += f", the stock is currently **down {change}%**" - else: - message += ", the stock hasn't shown any movement today." - else: - message = f"The symbol: {symbol} encountered and error. This could be due to " - - else: - message = f"The symbol: {symbol} was not found." - - if symbol.upper() == "GME": - message += "\n\n🙌💎Power to the Players💎🙌" - - dataMessages[symbol] = message - - return dataMessages - - def dividend_reply(self, symbol: str) -> Dict[str, str]: - """Returns the most recent, or next dividend date for a stock symbol. - - Parameters - ---------- - symbols : list - List of stock symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols div dates. - """ - - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" - response = r.get(IEXurl) - if response.status_code == 200: - IEXData = response.json()[0] - keys = ( - "amount", - "currency", - "declaredDate", - "exDate", - "frequency", - "paymentDate", - "flag", - ) - - if set(keys).issubset(IEXData): - - if IEXData["currency"] == "USD": - price = f"${IEXData['amount']}" - else: - price = f"{IEXData['amount']} {IEXData['currency']}" - - # Pattern IEX uses for dividend date. - pattern = "%Y-%m-%d" - - declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime( - "%A, %B %w" - ) - ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w") - payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime( - "%A, %B %w" - ) - - daysDelta = ( - datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now() - ).days - - return ( - f"The next dividend for ${self.symbol_list[self.symbol_list['symbol']==symbol.upper()]['description'].item()}" - + f" is on {payment} which is in {daysDelta} days." - + f" The dividend is for {price} per share." - + f"\nThe dividend was declared on {declared} and the ex-dividend date is {ex}" - ) - - return f"{symbol} either doesn't exist or pays no dividend." - - def news_reply(self, symbols: list) -> Dict[str, str]: - """Gets recent english news on stock symbols. - - Parameters - ---------- - symbols : list - List of stock symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable markdown formatted string of the symbols news. - """ - newsMessages = {} - - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/5?token={self.IEX_TOKEN}" - response = r.get(IEXurl) - if response.status_code == 200: - data = response.json() - if len(data): - newsMessages[symbol] = f"News for **{symbol.upper()}**:\n\n" - for news in data: - if news["lang"] == "en" and not news["hasPaywall"]: - message = f"*{news['source']}*: [{news['headline']}]({news['url']})\n" - newsMessages[symbol] = newsMessages[symbol] + message - else: - newsMessages[ - symbol - ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." - else: - newsMessages[ - symbol - ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." - - return newsMessages - - def info_reply(self, symbols: List[str]) -> Dict[str, str]: - """Gets information on stock symbols. - - Parameters - ---------- - symbols : List[str] - List of stock symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols information. - """ - infoMessages = {} - - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" - response = r.get(IEXurl) - - if response.status_code == 200: - data = response.json() - infoMessages[symbol] = ( - f"Company Name: [{data['companyName']}]({data['website']})\nIndustry:" - + f" {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" - ) - - else: - infoMessages[ - symbol - ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - - return infoMessages - - def intra_reply(self, symbol: str) -> pd.DataFrame: - """Returns price data for a symbol since the last market open. - - Parameters - ---------- - symbol : str - Stock symbol. - - Returns - ------- - pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. - """ - if symbol.upper() not in list(self.symbol_list["symbol"]): - return pd.DataFrame() - - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}" - response = r.get(IEXurl) - if response.status_code == 200: - df = pd.DataFrame(response.json()) - df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) - df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) - df = df.set_index("DT") - return df - - return pd.DataFrame() - - def chart_reply(self, symbol: str) -> pd.DataFrame: - """Returns price data for a symbol of the past month up until the previous trading days close. - Also caches multiple requests made in the same day. - - Parameters - ---------- - symbol : str - Stock symbol. - - Returns - ------- - pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. - """ - schedule.run_pending() - - if symbol.upper() not in list(self.symbol_list["symbol"]): - return pd.DataFrame() - - try: # https://stackoverflow.com/a/3845776/8774114 - return self.charts[symbol.upper()] - except KeyError: - pass - - response = r.get( - f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false" - ) - - if response.status_code == 200: - df = pd.DataFrame(response.json()) - df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) - df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) - df = df.set_index("DT") - self.charts[symbol.upper()] = df - return df - - return pd.DataFrame() - - def stat_reply(self, symbols: List[str]) -> Dict[str, str]: - """Gets key statistics for each symbol in the list - - Parameters - ---------- - symbols : List[str] - List of stock symbols - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics. - """ - infoMessages = {} - - for symbol in symbols: - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" - response = r.get(IEXurl) - - if response.status_code == 200: - data = response.json() - [data.pop(k) for k in list(data) if data[k] == ""] - - m = "" - if "companyName" in data: - m += f"Company Name: {data['companyName']}\n" - if "marketcap" in data: - m += f"Market Cap: {data['marketcap']:,}\n" - if "week52high" in data: - m += f"52 Week (high-low): {data['week52high']:,} " - if "week52low" in data: - m += f"- {data['week52low']:,}\n" - if "employees" in data: - m += f"Number of Employees: {data['employees']:,}\n" - if "nextEarningsDate" in data: - m += f"Next Earnings Date: {data['nextEarningsDate']}\n" - if "peRatio" in data: - m += f"Price to Earnings: {data['peRatio']:.3f}\n" - if "beta" in data: - m += f"Beta: {data['beta']:.3f}\n" - infoMessages[symbol] = m - else: - infoMessages[ - symbol - ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - - return infoMessages - - def crypto_reply(self, pair: str) -> str: - """Returns the current price of a cryptocurrency - - Parameters - ---------- - pair : str - symbol for the cryptocurrency, sometimes with a price pair like ETHUSD - - Returns - ------- - str - Returns a human readable markdown description of the price, or an empty string if no price was found. - """ - - pair = pair.split(" ")[-1].replace("/", "").upper() - pair += "USD" if len(pair) == 3 else pair - - IEXurl = f"https://cloud.iexapis.com/stable/crypto/{pair}/quote?token={self.IEX_TOKEN}" - - response = r.get(IEXurl) - - if response.status_code == 200: - data = response.json() - - quote = f"Symbol: {data['symbol']}\n" - quote += f"Price: ${data['latestPrice']}\n" - - new, old = data["latestPrice"], data["previousClose"] - if old is not None: - change = (float(new) - float(old)) / float(old) - quote += f"Change: {change}\n" - - return quote - - else: - return "" diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index 7b7e5d0f1..bf45fe5fa 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,8 +1,7 @@ -discord.py==1.7.3 +nextcord==2.0.0a5 requests==2.25.1 pandas==1.2.1 -fuzzywuzzy==0.18.0 -python-Levenshtein==0.12.1 schedule==1.0.0 mplfinance==0.12.7a5 -markdownify==0.6.5 \ No newline at end of file +markdownify==0.6.5 +cachetools==4.2.2 \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/symbol_router.py b/simple-stock-bots/simple-discord-stock-bot/symbol_router.py index efcecd4d9..d61ce66b0 100644 --- a/simple-stock-bots/simple-discord-stock-bot/symbol_router.py +++ b/simple-stock-bots/simple-discord-stock-bot/symbol_router.py @@ -2,12 +2,14 @@ """ import datetime +import logging import random import re from logging import critical, debug, error, info, warning import pandas as pd -from fuzzywuzzy import fuzz +import schedule +from cachetools import TTLCache, cached from cg_Crypto import cg_Crypto from IEX_Symbol import IEX_Symbol @@ -17,13 +19,33 @@ from Symbol import Coin, Stock, Symbol class Router: STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})" CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})" - searched_symbols = {} + trending_count = {} def __init__(self): self.stock = IEX_Symbol() self.crypto = cg_Crypto() - def find_symbols(self, text: str) -> list[Symbol]: + schedule.every().hour.do(self.trending_decay) + + def trending_decay(self, decay=0.5): + """Decays the value of each trending stock by a multiplier""" + t_copy = {} + dead_keys = [] + if self.trending_count: + t_copy = self.trending_count.copy() + for key in t_copy.keys(): + if t_copy[key] < 0.01: + # This just makes sure were not keeping around keys that havent been called in a very long time. + dead_keys.append(key) + else: + t_copy[key] = t_copy[key] * decay + for dead in dead_keys: + t_copy.pop(dead) + + self.trending_count = t_copy.copy() + info("Decayed trending symbols.") + + def find_symbols(self, text: str, *, trending_weight: int = 1) -> list[Symbol]: """Finds stock tickers starting with a dollar sign, and cryptocurrencies with two dollar signs in a blob of text and returns them in a list. @@ -34,25 +56,42 @@ class Router: Returns ------- - list[str] - List of stock symbols as strings without dollar sign. + list[Symbol] + List of stock symbols as Symbol objects """ + schedule.run_pending() + symbols = [] stocks = set(re.findall(self.STOCK_REGEX, text)) for stock in stocks: - if stock.upper() in self.stock.symbol_list["symbol"].values: - symbols.append(Stock(stock)) + sym = self.stock.symbol_list[ + self.stock.symbol_list["symbol"].str.fullmatch(stock, case=False) + ] + if ~sym.empty: + print(sym) + symbols.append(Stock(sym)) else: info(f"{stock} is not in list of stocks") coins = set(re.findall(self.CRYPTO_REGEX, text)) for coin in coins: - if coin.lower() in self.crypto.symbol_list["symbol"].values: - symbols.append(Coin(coin.lower())) + sym = self.crypto.symbol_list[ + self.crypto.symbol_list["symbol"].str.fullmatch( + coin.lower(), case=False + ) + ] + if ~sym.empty: + symbols.append(Coin(sym)) else: info(f"{coin} is not in list of coins") - info(symbols) - return symbols + if symbols: + info(symbols) + for symbol in symbols: + self.trending_count[symbol.tag] = ( + self.trending_count.get(symbol.tag, 0) + trending_weight + ) + + return symbols def status(self, bot_resp) -> str: """Checks for any issues with APIs. @@ -78,45 +117,7 @@ class Router: return stats - def search_symbols(self, search: str) -> list[tuple[str, str]]: - """Performs a fuzzy search to find stock symbols closest to a search term. - - Parameters - ---------- - search : str - String used to search, could be a company name or something close to the companies stock ticker. - - Returns - ------- - list[tuple[str, str]] - A list tuples of every stock sorted in order of how well they match. - Each tuple contains: (Symbol, Issue Name). - """ - - df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) - - search = search.lower() - - df["Match"] = df.apply( - lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), - axis=1, - ) - - df.sort_values(by="Match", ascending=False, inplace=True) - # if df["Match"].head().sum() < 300: - # df["Match"] = df.apply( - # lambda x: fuzz.partial_ratio(search, x["name"].lower()), - # axis=1, - # ) - - # df.sort_values(by="Match", ascending=False, inplace=True) - - symbols = df.head(20) - symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) - self.searched_symbols[search] = symbol_list - return symbol_list - - def inline_search(self, search: str) -> list[tuple[str, str]]: + def inline_search(self, search: str, matches: int = 5) -> pd.DataFrame: """Searches based on the shortest symbol that contains the same string as the search. Should be very fast compared to a fuzzy search. @@ -133,16 +134,16 @@ class Router: df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) - search = search.lower() + df = df[ + df["description"].str.contains(search, regex=False, case=False) + ].sort_values(by="type_id", key=lambda x: x.str.len()) - df = df[df["type_id"].str.contains(search, regex=False)].sort_values( - by="type_id", key=lambda x: x.str.len() + symbols = df.head(matches) + symbols["price_reply"] = symbols["type_id"].apply( + lambda sym: self.price_reply(self.find_symbols(sym, trending_weight=0))[0] ) - symbols = df.head(20) - symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) - self.searched_symbols[search] = symbol_list - return symbol_list + return symbols def price_reply(self, symbols: list[Symbol]) -> list[str]: """Returns current market price or after hours if its available for a given stock symbol. @@ -192,7 +193,7 @@ class Router: elif isinstance(symbol, Coin): replies.append("Cryptocurrencies do no have Dividends.") else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return replies @@ -221,7 +222,7 @@ class Router: "News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs" ) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return replies @@ -247,7 +248,7 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.info_reply(symbol)) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return replies @@ -271,7 +272,7 @@ class Router: elif isinstance(symbol, Coin): return self.crypto.intra_reply(symbol) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return pd.DataFrame() def chart_reply(self, symbol: Symbol) -> pd.DataFrame: @@ -294,7 +295,7 @@ class Router: elif isinstance(symbol, Coin): return self.crypto.chart_reply(symbol) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return pd.DataFrame() def stat_reply(self, symbols: list[Symbol]) -> list[str]: @@ -319,7 +320,7 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.stat_reply(symbol)) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return replies @@ -345,10 +346,36 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.cap_reply(symbol)) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") return replies + def spark_reply(self, symbols: list[Symbol]) -> list[str]: + """Gets change for each symbol and returns it in a compact format + + Parameters + ---------- + symbols : list[str] + List of stock symbols + + Returns + ------- + list[str] + List of human readable strings. + """ + replies = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.spark_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append(self.crypto.spark_reply(symbol)) + else: + debug(f"{symbol} is not a Stock or Coin") + + return replies + + @cached(cache=TTLCache(maxsize=1024, ttl=600)) def trending(self) -> str: """Checks APIs for trending symbols. @@ -361,17 +388,40 @@ class Router: stocks = self.stock.trending() coins = self.crypto.trending() - reply = "Trending Stocks:\n" - reply += "-" * len("Trending Stocks:") + "\n" - for stock in stocks: - reply += stock + "\n" + reply = "" - reply += "\n\nTrending Crypto:\n" - reply += "-" * len("Trending Crypto:") + "\n" - for coin in coins: - reply += coin + "\n" + if self.trending_count: + reply += "🔥Trending on the Stock Bot:\n`" + reply += "━" * len("Trending on the Stock Bot:") + "`\n" - return reply + sorted_trending = [ + s[0] + for s in sorted(self.trending_count.items(), key=lambda item: item[1]) + ][::-1][0:5] + + for t in sorted_trending: + reply += self.spark_reply(self.find_symbols(t))[0] + "\n" + + if stocks: + reply += "\n\n💵Trending Stocks:\n`" + reply += "━" * len("Trending Stocks:") + "`\n" + for stock in stocks: + reply += stock + "\n" + + if coins: + reply += "\n\n🦎Trending Crypto:\n`" + reply += "━" * len("Trending Crypto:") + "`\n" + for coin in coins: + reply += coin + "\n" + + if "`$GME" in reply: + reply = reply.replace("🔥", "🦍") + + if reply: + return reply + else: + warning("Failed to collect trending data.") + return "Trending data is not currently available." def random_pick(self) -> str: @@ -409,7 +459,7 @@ class Router: elif isinstance(symbol, Coin): coins.append(symbol) else: - print(f"{symbol} is not a Stock or Coin") + debug(f"{symbol} is not a Stock or Coin") if stocks: # IEX batch endpoint doesnt seem to be working right now -- 2.51.2 From c8c9c7f12ae94cc984acd2d51fc2519777ff7b77 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 16 Apr 2023 21:57:33 +0000 Subject: [PATCH 75/75] Resolve "Pull in changes from Telegram bot" --- .../.devcontainer/Dockerfile | 18 +- .../simple-discord-stock-bot/.gitignore | 4 +- .../simple-discord-stock-bot/.gitlab-ci.yml | 114 +-- .../.vscode/launch.json | 7 +- .../.vscode/settings.json | 12 +- .../simple-discord-stock-bot/D_info.py | 32 +- .../simple-discord-stock-bot/Dockerfile | 4 +- .../simple-discord-stock-bot/IEX_Symbol.py | 497 ------------ .../simple-discord-stock-bot/LICENSE | 194 +---- .../simple-discord-stock-bot/MarketData.py | 263 ++++++ .../simple-discord-stock-bot/README.md | 175 +--- .../simple-discord-stock-bot/Symbol.py | 16 +- .../simple-discord-stock-bot/bot.py | 76 +- .../simple-discord-stock-bot/cg_Crypto.py | 755 +++++++++--------- .../simple-discord-stock-bot/dev-reqs.txt | 8 + .../simple-discord-stock-bot/pyproject.toml | 8 + .../simple-discord-stock-bot/requirements.txt | 4 +- .../simple-discord-stock-bot/symbol_router.py | 156 +--- .../simple-discord-stock-bot/tests.py | 33 + 19 files changed, 832 insertions(+), 1544 deletions(-) delete mode 100644 simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/MarketData.py create mode 100644 simple-stock-bots/simple-discord-stock-bot/dev-reqs.txt create mode 100644 simple-stock-bots/simple-discord-stock-bot/pyproject.toml create mode 100644 simple-stock-bots/simple-discord-stock-bot/tests.py diff --git a/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile index 04540b27a..4c73e8931 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/.devcontainer/Dockerfile @@ -1,23 +1,21 @@ -FROM python:3.9-buster AS builder +FROM python:3.11-buster AS builder COPY requirements.txt /requirements.txt -RUN pip install --user -r requirements.txt +COPY dev-reqs.txt /dev-reqs.txt +RUN pip install --user -r dev-reqs.txt -FROM python:3.9-slim +FROM python:3.11-slim -ENV MPLBACKEND=Agg COPY --from=builder /root/.local /root/.local -# Formatting -RUN pip install --no-cache-dir black -# Jupyter Notebooks -RUN pip install --no-cache-dir ipykernel -ENV DISCORD=TOKEN -ENV IEX=TOKEN +ENV MPLBACKEND=Agg + +ENV DISCORD=NTMyMDQ1MjAwODIzMDI1NjY2.XDQftA.Px-arL5wDMB4XKcoPOS1r4gCGmA +ENV MARKETDATA=a01mVUZ4cW1sUUFOVWlEZ3NNTHFNeHYzS2diUUhTUVJZbzNxVVEwTUxVMD0 COPY . . diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitignore b/simple-stock-bots/simple-discord-stock-bot/.gitignore index ed8ebf583..14a295ab9 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.gitignore +++ b/simple-stock-bots/simple-discord-stock-bot/.gitignore @@ -1 +1,3 @@ -__pycache__ \ No newline at end of file +__pycache__ +.mypy_cache +nextcord.log \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml b/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml index 1f5ae5c04..b6455bd27 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml +++ b/simple-stock-bots/simple-discord-stock-bot/.gitlab-ci.yml @@ -1,96 +1,34 @@ -#Following instructions (as of 2020-04-01): https://docs.gitlab.com/ee/ci/docker/using_kaniko.html -#Kaniko docs are here: https://github.com/GoogleContainerTools/kaniko -#While this example shows building to multiple registries for all branches, with a few modifications -# it can be used to build non-master branches to a "dev" container registry and only build master to -# a production container registry - -image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] - -variables: - #More Information on Kaniko Caching: https://cloud.google.com/build/docs/kaniko-cache - KANIKO_CACHE_ARGS: "--cache=true --cache-copy-layers=true --cache-ttl=24h" - VERSIONLABELMETHOD: "OnlyIfThisCommitHasVersion" # options: "OnlyIfThisCommitHasVersion","LastVersionTagInGit" - IMAGE_LABELS: > - --label org.opencontainers.image.vendor=$CI_SERVER_URL/$GITLAB_USER_LOGIN - --label org.opencontainers.image.authors=$CI_SERVER_URL/$GITLAB_USER_LOGIN - --label org.opencontainers.image.revision=$CI_COMMIT_SHA - --label org.opencontainers.image.source=$CI_PROJECT_URL - --label org.opencontainers.image.documentation=$CI_PROJECT_URL - --label org.opencontainers.image.licenses=$CI_PROJECT_URL - --label org.opencontainers.image.url=$CI_PROJECT_URL - --label vcs-url=$CI_PROJECT_URL - --label com.gitlab.ci.user=$CI_SERVER_URL/$GITLAB_USER_LOGIN - --label com.gitlab.ci.email=$GITLAB_USER_EMAIL - --label com.gitlab.ci.tagorbranch=$CI_COMMIT_REF_NAME - --label com.gitlab.ci.pipelineurl=$CI_PIPELINE_URL - --label com.gitlab.ci.commiturl=$CI_PROJECT_URL/commit/$CI_COMMIT_SHA - --label com.gitlab.ci.cijoburl=$CI_JOB_URL - --label com.gitlab.ci.mrurl=$CI_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_ID - -get-latest-git-version: +black: stage: .pre - image: - name: alpine/git - entrypoint: [""] - rules: - - if: '$VERSIONLABELMETHOD == "LastVersionTagInGit"' + image: registry.gitlab.com/pipeline-components/black:latest script: - - | - echo "the google kaniko container does not have git and does not have a packge manager to install it" - git clone https://github.com/GoogleContainerTools/kaniko.git - cd kaniko - echo "$(git describe --abbrev=0 --tags)" > ../VERSIONTAG.txt - echo "VERSIONTAG.txt contains $(cat ../VERSIONTAG.txt)" - artifacts: - paths: - - VERSIONTAG.txt + - black --check --verbose -- . -.build_with_kaniko: - #Hidden job to use as an "extends" template +build:master: stage: build + image: + name: gcr.io/kaniko-project/executor:v1.9.0-debug + entrypoint: [""] script: - - | - echo "Building and shipping image to $CI_REGISTRY_IMAGE" - #Build date for opencontainers - BUILDDATE="'$(date '+%FT%T%z' | sed -E -n 's/(\+[0-9]{2})([0-9]{2})$/\1:\2/p')'" #rfc 3339 date - IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.created=$BUILDDATE --label build-date=$BUILDDATE" - #Description for opencontainers - BUILDTITLE=$(echo $CI_PROJECT_TITLE | tr " " "_") - IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.title=$BUILDTITLE --label org.opencontainers.image.description=$BUILDTITLE" - #Add ref.name for opencontainers - IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.ref.name=$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" - - #Build Version Label and Tag from git tag, LastVersionTagInGit was placed by a previous job artifact - if [[ "$VERSIONLABELMETHOD" == "LastVersionTagInGit" ]]; then VERSIONLABEL=$(cat VERSIONTAG.txt); fi - if [[ "$VERSIONLABELMETHOD" == "OnlyIfThisCommitHasVersion" ]]; then VERSIONLABEL=$CI_COMMIT_TAG; fi - if [[ ! -z "$VERSIONLABEL" ]]; then - IMAGE_LABELS="$IMAGE_LABELS --label org.opencontainers.image.version=$VERSIONLABEL" - ADDITIONALTAGLIST="$ADDITIONALTAGLIST $VERSIONLABEL" - fi - - ADDITIONALTAGLIST="$ADDITIONALTAGLIST $CI_COMMIT_REF_NAME $CI_COMMIT_SHORT_SHA" - if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then ADDITIONALTAGLIST="$ADDITIONALTAGLIST latest"; fi - if [[ -n "$ADDITIONALTAGLIST" ]]; then - for TAG in $ADDITIONALTAGLIST; do - FORMATTEDTAGLIST="${FORMATTEDTAGLIST} --tag $CI_REGISTRY_IMAGE:$TAG "; - done; - fi - - #Reformat Docker tags to kaniko's --destination argument: - FORMATTEDTAGLIST=$(echo "${FORMATTEDTAGLIST}" | sed s/\-\-tag/\-\-destination/g) - - echo "Kaniko arguments to run: --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile $KANIKO_CACHE_ARGS $FORMATTEDTAGLIST $IMAGE_LABELS" - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile $KANIKO_CACHE_ARGS $FORMATTEDTAGLIST $IMAGE_LABELS + - /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/Dockerfile" + --destination "${CI_REGISTRY_IMAGE}:latest" + rules: + - if: '$CI_COMMIT_BRANCH == "master"' -build-for-gitlab-project-registry: - extends: .build_with_kaniko - environment: - #This is only here for completeness, since there are no CI CD Variables with this scope, the project defaults are used - # to push to this projects docker registry - name: push-to-gitlab-project-registry \ No newline at end of file +build:branch: + stage: build + image: + name: gcr.io/kaniko-project/executor:v1.9.0-debug + entrypoint: [""] + script: + - /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/Dockerfile" + --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}" + --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_BRANCH}" + rules: + - if: '$CI_COMMIT_BRANCH != "master"' \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json b/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json index a49a5a1ae..7afd2996f 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json +++ b/simple-stock-bots/simple-discord-stock-bot/.vscode/launch.json @@ -1,15 +1,12 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", "configurations": [ { "name": "Discord Bot", "type": "python", "request": "launch", "program": "bot.py", - "console": "integratedTerminal" + "console": "integratedTerminal", + "python": "python3.11" } ] } \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json b/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json index 2c898aeb4..e2640e2ee 100644 --- a/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json +++ b/simple-stock-bots/simple-discord-stock-bot/.vscode/settings.json @@ -1,13 +1,5 @@ { - "workbench.iconTheme": "vscode-icons", - "editor.suggestSelection": "first", - "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", - "python.languageServer": "Pylance", - "git.autofetch": true, - "editor.formatOnSave": true, - "files.associations": { - "DockerDev": "dockerfile", - }, "python.formatting.provider": "black", - "python.showStartPage": false, + "python.linting.mypyEnabled": true, + "python.linting.flake8Enabled": true, } \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/D_info.py b/simple-stock-bots/simple-discord-stock-bot/D_info.py index 5ccb85efd..58f590da2 100644 --- a/simple-stock-bots/simple-discord-stock-bot/D_info.py +++ b/simple-stock-bots/simple-discord-stock-bot/D_info.py @@ -1,4 +1,4 @@ -"""Functions and Info specific to the Telegram Bot +"""Functions and Info specific to the discord Bot """ import re @@ -10,15 +10,15 @@ class D_info: license = re.sub( r"\b\n", " ", - r.get( - "https://gitlab.com/simple-stock-bots/simple-discord-stock-bot/-/raw/master/LICENSE" - ).text, + r.get("https://gitlab.com/simple-stock-bots/simple-discord-stock-bot/-/raw/master/LICENSE").text, ) help_text = """ Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) -Keep up with the latest news for the bot in its Telegram Channel: https://t.me/simplestockbotnews +If you are interested in stock market data, or want to host your own bot, be sure to use my affiliate link so that the bot can stay free: [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord) + +Keep up with the latest news for the bot in its discord Channel: https://t.me/simplestockbotnews Full documentation on using and running your own stock bot can be found on the bots [docs.](https://docs.simplestockbot.com) @@ -28,18 +28,15 @@ Simply calling a symbol in any message that the bot can see will also return the **Commands** - `/donate [amount in USD]` to donate. 🎗️ - - `/dividend $[symbol]` Dividend information for the symbol. 📅 - `/intra $[symbol]` Plot of the stocks movement since the last market open. 📈 - `/chart $[symbol]` Plot of the stocks movement for the past 1 month. 📊 - - `/news $[symbol]` News about the symbol. 📰 - - `/info $[symbol]` General information about the symbol. ℹ️ - - `/stat $[symbol]` Key statistics about the symbol. 🔢 - - `/cap $[symbol]` Market Capitalization of symbol. 💰 - `/trending` Trending Stocks and Cryptos. 💬 - `/help` Get some help using the bot. 🆘 - - Market data is provided by [IEX Cloud](https://iexcloud.io) +**Inline Features** + You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock and crypto symbols and return the price. Then once you select the ticker want the bot will send a message as you in that chat with the latest stock price. Prices may be delayed by up to an hour. + + Market data is provided by [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord) If you believe the bot is not behaving properly run `/status` or [get in touch](https://docs.simplestockbot.com/contact). """ @@ -47,7 +44,7 @@ Simply calling a symbol in any message that the bot can see will also return the donate_text = """ Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson) All donations go directly towards paying for servers, and market data is provided by -[IEX Cloud](https://iexcloud.io/). +[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord). The easiest way to donate is to run the `/donate [amount in USD]` command with US dollars you would like to donate. @@ -59,15 +56,10 @@ If you have any questions see the [website](https://docs.simplestockbot.com) """ -commands = """ +commands = """ # Not used by the bot but for updating commands with BotFather donate - Donate to the bot 🎗️ help - Get some help using the bot. 🆘 -info - $[symbol] General information about the symbol. ℹ️ -news - $[symbol] News about the symbol. 📰 -stat - $[symbol] Key statistics about the symbol. 🔢 -cap - $[symbol] Market Capitalization of symbol. 💰 -dividend - $[symbol] Dividend info 📅 trending - Trending Stocks and Cryptos. 💬 intra - $[symbol] Plot since the last market open. 📈 chart - $[chart] Plot of the past month. 📊 -""" # Not used by the bot but for updaing commands with BotFather +""" diff --git a/simple-stock-bots/simple-discord-stock-bot/Dockerfile b/simple-stock-bots/simple-discord-stock-bot/Dockerfile index 7ae49d2fe..c175b3262 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Dockerfile +++ b/simple-stock-bots/simple-discord-stock-bot/Dockerfile @@ -1,11 +1,11 @@ -FROM python:3.9-buster AS builder +FROM python:3.11-buster AS builder COPY requirements.txt /requirements.txt RUN pip install --user -r requirements.txt -FROM python:3.9-slim +FROM python:3.11-slim ENV MPLBACKEND=Agg diff --git a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py b/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py deleted file mode 100644 index 6b933593d..000000000 --- a/simple-stock-bots/simple-discord-stock-bot/IEX_Symbol.py +++ /dev/null @@ -1,497 +0,0 @@ -"""Class with functions for running the bot with IEX Cloud. -""" - -import logging -import os -from datetime import datetime -from logging import warning -from typing import List, Optional, Tuple - -import pandas as pd -import requests as r -import schedule - -from Symbol import Stock - - -class IEX_Symbol: - """ - Functions for finding stock market information about symbols. - """ - - SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" - - searched_symbols = {} - otc_list = [] - charts = {} - trending_cache = None - - def __init__(self) -> None: - """Creates a Symbol Object - - Parameters - ---------- - IEX_TOKEN : str - IEX API Token - """ - try: - self.IEX_TOKEN = os.environ["IEX"] - - if self.IEX_TOKEN == "TOKEN": - self.IEX_TOKEN = "" - except KeyError: - self.IEX_TOKEN = "" - warning( - "Starting without an IEX Token will not allow you to get market data!" - ) - - if self.IEX_TOKEN != "": - self.get_symbol_list() - - schedule.every().day.do(self.get_symbol_list) - schedule.every().day.do(self.clear_charts) - - def get(self, endpoint, params: dict = {}, timeout=5) -> dict: - - url = "https://cloud.iexapis.com/stable" + endpoint - - # set token param if it wasn't passed. - params["token"] = params.get("token", self.IEX_TOKEN) - - resp = r.get(url, params=params, timeout=timeout) - - # Make sure API returned a proper status code - try: - resp.raise_for_status() - except r.exceptions.HTTPError as e: - logging.error(e) - return {} - - # Make sure API returned valid JSON - try: - resp_json = resp.json() - - # IEX uses backtick ` as apostrophe which breaks telegram markdown parsing - if type(resp_json) is dict: - resp_json["companyName"] = resp_json.get("companyName", "").replace( - "`", "'" - ) - - return resp_json - except r.exceptions.JSONDecodeError as e: - logging.error(e) - return {} - - def clear_charts(self) -> None: - """ - Clears cache of chart data. - Charts are cached so that only 1 API call per 24 hours is needed since the - chart data is expensive and a large download. - """ - self.charts = {} - - def get_symbol_list( - self, return_df=False - ) -> Optional[Tuple[pd.DataFrame, datetime]]: - """Gets list of all symbols supported by IEX - - Parameters - ---------- - return_df : bool, optional - return the dataframe of all stock symbols, by default False - - Returns - ------- - Optional[Tuple[pd.DataFrame, datetime]] - If `return_df` is set to `True` returns a dataframe, otherwise returns `None`. - """ - - reg_symbols = self.get("/ref-data/symbols") - otc_symbols = self.get("/ref-data/otc/symbols") - - reg = pd.DataFrame(data=reg_symbols) - otc = pd.DataFrame(data=otc_symbols) - self.otc_list = set(otc["symbol"].to_list()) - - symbols = pd.concat([reg, otc]) - - # IEX uses backtick ` as apostrophe which breaks telegram markdown parsing - symbols["name"] = symbols["name"].str.replace("`", "'") - - symbols["description"] = "$" + symbols["symbol"] + ": " + symbols["name"] - symbols["id"] = symbols["symbol"] - symbols["type_id"] = "$" + symbols["symbol"].str.lower() - - symbols = symbols[["id", "symbol", "name", "description", "type_id"]] - self.symbol_list = symbols - if return_df: - return symbols, datetime.now() - - def status(self) -> str: - """Checks IEX Status dashboard for any current API issues. - - Returns - ------- - str - Human readable text on status of IEX API - """ - - if self.IEX_TOKEN == "": - return "The `IEX_TOKEN` is not set so Stock Market data is not available." - - resp = r.get( - "https://pjmps0c34hp7.statuspage.io/api/v2/status.json", - timeout=15, - ) - - if resp.status_code == 200: - status = resp.json()["status"] - else: - return "IEX Cloud did not respond. Please check their status page for more information. https://status.iexapis.com" - - if status["indicator"] == "none": - return "IEX Cloud is currently not reporting any issues with its API." - else: - return ( - f"{status['indicator']}: {status['description']}." - + " Please check the status page for more information. https://status.iexapis.com" - ) - - def price_reply(self, symbol: Stock) -> str: - """Returns price movement of Stock for the last market day, or after hours. - - Parameters - ---------- - symbol : Stock - - Returns - ------- - str - Formatted markdown - """ - - if IEXData := self.get(f"/stock/{symbol.id}/quote"): - - if symbol.symbol.upper() in self.otc_list: - return f"OTC - {symbol.symbol.upper()}, {IEXData['companyName']} most recent price is: $**{IEXData['latestPrice']}**" - - keys = ( - "extendedChangePercent", - "extendedPrice", - "companyName", - "latestPrice", - "changePercent", - ) - - if set(keys).issubset(IEXData): - - if change := IEXData.get("changePercent", 0): - change = round(change * 100, 2) - else: - change = 0 - - if ( - IEXData.get("isUSMarketOpen", True) - or (IEXData["extendedChangePercent"] is None) - or (IEXData["extendedPrice"] is None) - ): # Check if market is open. - message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" - else: - message = ( - f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}** with a change of {change}%," - + f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**" - ) - if change := IEXData.get("extendedChangePercent", 0): - change = round(change * 100, 2) - else: - change = 0 - - # Determine wording of change text - if change > 0: - message += f", the stock is currently **up {change}%**" - elif change < 0: - message += f", the stock is currently **down {change}%**" - else: - message += ", the stock hasn't shown any movement today." - else: - message = ( - f"The symbol: {symbol} encountered and error. This could be due to " - ) - - else: - message = f"The symbol: {symbol} was not found." - - return message - - def dividend_reply(self, symbol: Stock) -> str: - """Returns the most recent, or next dividend date for a stock symbol. - - Parameters - ---------- - symbol : Stock - - Returns - ------- - str - Formatted markdown - """ - if symbol.symbol.upper() in self.otc_list: - return "OTC stocks do not currently support any commands." - - if resp := self.get(f"/stock/{symbol.id}/dividends/next"): - try: - IEXData = resp[0] - except IndexError as e: - logging.info(e) - return f"Getting dividend information for ${symbol.id.upper()} encountered an error. The provider for upcoming dividend information has been having issues recently which has likely caused this error. It is also possible that the stock has no dividend or does not exist." - keys = ( - "amount", - "currency", - "declaredDate", - "exDate", - "frequency", - "paymentDate", - "flag", - ) - - if set(keys).issubset(IEXData): - - if IEXData["currency"] == "USD": - price = f"${IEXData['amount']}" - else: - price = f"{IEXData['amount']} {IEXData['currency']}" - - # Pattern IEX uses for dividend date. - pattern = "%Y-%m-%d" - - declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime( - "%A, %B %w" - ) - ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w") - payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime( - "%A, %B %w" - ) - - daysDelta = ( - datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now() - ).days - - return ( - "The next dividend for " - + f"{self.symbol_list[self.symbol_list['symbol']==symbol.id.upper()]['description'].item()}" # Get full name without api call - + f" is on {payment} which is in {daysDelta} days." - + f" The dividend is for {price} per share." - + f"\n\nThe dividend was declared on {declared} and the ex-dividend date is {ex}" - ) - - return f"Getting dividend information for ${symbol.id.upper()} encountered an error. The provider for upcoming dividend information has been having issues recently which has likely caused this error. It is also possible that the stock has no dividend or does not exist." - - def news_reply(self, symbol: Stock) -> str: - """Gets most recent, english, non-paywalled news - - Parameters - ---------- - symbol : Stock - - Returns - ------- - str - Formatted markdown - """ - if symbol.symbol.upper() in self.otc_list: - return "OTC stocks do not currently support any commands." - - if data := self.get(f"/stock/{symbol.id}/news/last/15"): - line = [] - - for news in data: - if news["lang"] == "en" and not news["hasPaywall"]: - line.append( - f"*{news['source']}*: [{news['headline']}]({news['url']})" - ) - - return f"News for **{symbol.id.upper()}**:\n" + "\n".join(line[:5]) - - else: - return f"No news found for: {symbol.id}\nEither today is boring or the symbol does not exist." - - def info_reply(self, symbol: Stock) -> str: - """Gets description for Stock - - Parameters - ---------- - symbol : Stock - - Returns - ------- - str - Formatted text - """ - if symbol.symbol.upper() in self.otc_list: - return "OTC stocks do not currently support any commands." - - if data := self.get(f"/stock/{symbol.id}/company"): - [data.pop(k) for k in list(data) if data[k] == ""] - - if "description" in data: - return data["description"] - - return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - - def stat_reply(self, symbol: Stock) -> str: - """Key statistics on a Stock - - Parameters - ---------- - symbol : Stock - - Returns - ------- - str - Formatted markdown - """ - if symbol.symbol.upper() in self.otc_list: - return "OTC stocks do not currently support any commands." - - if data := self.get(f"/stock/{symbol.id}/stats"): - [data.pop(k) for k in list(data) if data[k] == ""] - - m = "" - if "companyName" in data: - m += f"Company Name: {data['companyName']}\n" - if "marketcap" in data: - m += f"Market Cap: ${data['marketcap']:,}\n" - if "week52high" in data: - m += f"52 Week (high-low): {data['week52high']:,} " - if "week52low" in data: - m += f"- {data['week52low']:,}\n" - if "employees" in data: - m += f"Number of Employees: {data['employees']:,}\n" - if "nextEarningsDate" in data: - m += f"Next Earnings Date: {data['nextEarningsDate']}\n" - if "peRatio" in data: - m += f"Price to Earnings: {data['peRatio']:.3f}\n" - if "beta" in data: - m += f"Beta: {data['beta']:.3f}\n" - return m - else: - return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - - def cap_reply(self, symbol: Stock) -> str: - """Get the Market Cap of a stock""" - - if data := self.get(f"/stock/{symbol.id}/stats"): - - try: - cap = data["marketcap"] - except KeyError: - return f"{symbol.id} returned an error." - - message = f"The current market cap of {symbol.name} is $**{cap:,.2f}**" - - else: - message = f"The Stock: {symbol.name} was not found or returned and error." - - return message - - def intra_reply(self, symbol: Stock) -> pd.DataFrame: - """Returns price data for a symbol since the last market open. - - Parameters - ---------- - symbol : str - Stock symbol. - - Returns - ------- - pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. - """ - if symbol.symbol.upper() in self.otc_list: - return pd.DataFrame() - - if symbol.id.upper() not in list(self.symbol_list["symbol"]): - return pd.DataFrame() - - if data := self.get(f"/stock/{symbol.id}/intraday-prices"): - df = pd.DataFrame(data) - df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) - df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) - df = df.set_index("DT") - return df - - return pd.DataFrame() - - def chart_reply(self, symbol: Stock) -> pd.DataFrame: - """Returns price data for a symbol of the past month up until the previous trading days close. - Also caches multiple requests made in the same day. - - Parameters - ---------- - symbol : str - Stock symbol. - - Returns - ------- - pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. - """ - schedule.run_pending() - - if symbol.symbol.upper() in self.otc_list: - return pd.DataFrame() - - if symbol.id.upper() not in list(self.symbol_list["symbol"]): - return pd.DataFrame() - - try: # https://stackoverflow.com/a/3845776/8774114 - return self.charts[symbol.id.upper()] - except KeyError: - pass - - if data := self.get( - f"/stock/{symbol.id}/chart/1mm", - params={"chartInterval": 3, "includeToday": "false"}, - ): - df = pd.DataFrame(data) - df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) - df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) - df = df.set_index("DT") - self.charts[symbol.id.upper()] = df - return df - - return pd.DataFrame() - - def spark_reply(self, symbol: Stock) -> str: - quote = self.get(f"/stock/{symbol.id}/quote") - - open_change = quote.get("changePercent", 0) - after_change = quote.get("extendedChangePercent", 0) - - change = 0 - - if open_change: - change = change + open_change - if after_change: - change = change + after_change - - change = change * 100 - - return f"`{symbol.tag}`: {quote['companyName']}, {change:.2f}%" - - def trending(self) -> list[str]: - """Gets current coins trending on IEX. Only returns when market is open. - - Returns - ------- - list[str] - list of $ID: NAME, CHANGE% - """ - - if data := self.get(f"/stock/market/list/mostactive"): - self.trending_cache = [ - f"`${s['symbol']}`: {s['companyName']}, {100*s['changePercent']:.2f}%" - for s in data - ] - - return self.trending_cache diff --git a/simple-stock-bots/simple-discord-stock-bot/LICENSE b/simple-stock-bots/simple-discord-stock-bot/LICENSE index 033217724..18a8b7365 100644 --- a/simple-stock-bots/simple-discord-stock-bot/LICENSE +++ b/simple-stock-bots/simple-discord-stock-bot/LICENSE @@ -1,173 +1,21 @@ -## creative commons - -# Attribution-ShareAlike 4.0 International - -Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. - -### Using Creative Commons Public Licenses - -Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. - -* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). - -* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). - -## Creative Commons Attribution-ShareAlike 4.0 International Public License - -By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - -### Section 1 – Definitions. - -a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. - -b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. - -c. __BY-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. - -d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. - -e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. - -f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. - -g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. - -h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. - -i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. - -j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. - -k. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. - -l. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. - -m. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. - -### Section 2 – Scope. - -a. ___License grant.___ - - 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: - - A. reproduce and Share the Licensed Material, in whole or in part; and - - B. produce, reproduce, and Share Adapted Material. - - 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. - - 3. __Term.__ The term of this Public License is specified in Section 6(a). - - 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. - - 5. __Downstream recipients.__ - - A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. - - B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. - - C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. - - 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - -b. ___Other rights.___ - - 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this Public License. - - 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. - -### Section 3 – License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the following conditions. - -a. ___Attribution.___ - - 1. If You Share the Licensed Material (including in modified form), You must: - - A. retain the following if it is supplied by the Licensor with the Licensed Material: - - i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of warranties; - - v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; - - B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and - - C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. - - 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. - -b. ___ShareAlike.___ - -In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. - -1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. - -2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. - -3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. - -### Section 4 – Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - -a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; - -b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and - -c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. - -### Section 5 – Disclaimer of Warranties and Limitation of Liability. - -a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ - -b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ - -c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. - -### Section 6 – Term and Termination. - -a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. - -b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or - - 2. upon express reinstatement by the Licensor. - - For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. - -c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. - -d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. - -### Section 7 – Other Terms and Conditions. - -a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. - -b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. - -### Section 8 – Interpretation. - -a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. - -b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. - -c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. - -d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. - -> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the [CC0 Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/legalcode). Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. -> -> Creative Commons may be contacted at creativecommons.org. +MIT License + +Copyright (c) 2019 Anson Biggs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/simple-stock-bots/simple-discord-stock-bot/MarketData.py b/simple-stock-bots/simple-discord-stock-bot/MarketData.py new file mode 100644 index 000000000..1ebd4d102 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/MarketData.py @@ -0,0 +1,263 @@ +import datetime as dt +import logging +import os +from typing import Dict + +import pandas as pd +import pytz +import requests as r +import schedule + +from Symbol import Stock + +log = logging.getLogger(__name__) + + +class MarketData: + """ + Functions for finding stock market information about symbols from MarkData.app + """ + + SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" + + charts: Dict[Stock, pd.DataFrame] = {} + + openTime = dt.time(hour=9, minute=30, second=0) + marketTimeZone = pytz.timezone("US/Eastern") + + def __init__(self) -> None: + """Creates a Symbol Object + + Parameters + ---------- + MARKETDATA_TOKEN : str + MarketData.app API Token + """ + + try: + self.MARKETDATA_TOKEN = os.environ["MARKETDATA"] + + if self.MARKETDATA_TOKEN == "TOKEN": + self.MARKETDATA_TOKEN = "" + except KeyError: + self.MARKETDATA_TOKEN = "" + log.warning("Starting without an MarketData.app Token will not allow you to get market data!") + log.warning("Use this affiliate link so that the bot can stay free:") + log.warning("https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=repo") + + if self.MARKETDATA_TOKEN != "": + schedule.every().day.do(self.clear_charts) + + def get(self, endpoint, params: dict = {}, timeout=10) -> dict: + url = "https://api.marketdata.app/v1/" + endpoint + + # set token param if it wasn't passed. + params["token"] = self.MARKETDATA_TOKEN + + # Undocumented query variable that ensures bot usage can be + # monitored even if someone doesn't make it through an affiliate link. + params["application"] = "simplestockbot" + + resp = r.get(url, params=params, timeout=timeout) + + # Make sure API returned a proper status code + try: + resp.raise_for_status() + except r.exceptions.HTTPError as e: + logging.error(e) + return {} + + # Make sure API returned valid JSON + try: + resp_json = resp.json() + + match resp_json["s"]: + case "ok": + return resp_json + case "no_data": + return resp_json + case "error": + logging.error("MarketData Error:\n" + resp_json["errmsg"]) + return {} + + except r.exceptions.JSONDecodeError as e: + logging.error(e) + + return {} + + def clear_charts(self) -> None: + """ + Clears cache of chart data. + Charts are cached so that only 1 API call per 24 hours is needed since the + chart data is expensive and a large download. + """ + self.charts = {} + + def status(self) -> str: + # TODO: At the moment this API is poorly documented, this function likely needs to be revisited later. + + try: + status = r.get( + "https://stats.uptimerobot.com/api/getMonitorList/6Kv3zIow0A", + timeout=5, + ) + status.raise_for_status() + except r.HTTPError: + return f"API returned an HTTP error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." + except r.Timeout: + return "API timed out before it was able to give status. This is likely due to a surge in usage or a complete outage." + + statusJSON = status.json() + + if statusJSON["status"] == "ok": + return ( + f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} Seconds." + ) + else: + return f"MarketData.app is currently reporting the following status: {statusJSON['status']}" + + def price_reply(self, symbol: Stock) -> str: + """Returns price movement of Stock for the last market day, or after hours. + + Parameters + ---------- + symbol : Stock + + Returns + ------- + str + Formatted markdown + """ + + if quoteResp := self.get(f"stocks/quotes/{symbol}/"): + price = round(quoteResp["last"][0], 2) + changePercent = round(quoteResp["changepct"][0], 2) + + message = f"The current price of {symbol.name} is ${price} and " + + if changePercent > 0.0: + message += f"is currently up {changePercent}% for the day." + elif changePercent < 0.0: + message += f"is currently down {changePercent}% for the day." + else: + message += "hasn't shown any movement for the day." + + return message + else: + return f"Getting a quote for {symbol} encountered an error." + + def spark_reply(self, symbol: Stock) -> str: + if quoteResp := self.get(f"stocks/quotes/{symbol}/"): + changePercent = round(quoteResp["changepct"][0], 2) + return f"`{symbol.tag}`: {changePercent}%" + else: + logging.warning(f"{symbol} did not have 'changepct' field.") + return f"`{symbol.tag}`" + + def intra_reply(self, symbol: Stock) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + schedule.run_pending() + + try: + return self.charts[symbol.id.upper()] + except KeyError: + pass + + resolution = "15" # minutes + now = dt.datetime.now(self.marketTimeZone) + + if self.openTime < now.time(): + startTime = now.replace(hour=9, minute=30) + else: + startTime = now - dt.timedelta(days=1) + + if data := self.get( + f"stocks/candles/{resolution}/{symbol}", + params={"from": startTime.timestamp(), "to": now.timestamp(), "extended": True}, + ): + data.pop("s") + df = pd.DataFrame(data) + df["t"] = pd.to_datetime(df["t"], unit="s", utc=True) + df.set_index("t", inplace=True) + + df.rename( + columns={ + "o": "Open", + "h": "High", + "l": "Low", + "c": "Close", + "v": "Volume", + }, + inplace=True, + ) + + self.charts[symbol.id.upper()] = df + return df + + return pd.DataFrame() + + def chart_reply(self, symbol: Stock) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + schedule.run_pending() + + try: + return self.charts[symbol.id.upper()] + except KeyError: + pass + + to_date = dt.datetime.today().strftime("%Y-%m-%d") + from_date = (dt.datetime.today() - dt.timedelta(days=30)).strftime("%Y-%m-%d") + resultion = "daily" + + if data := self.get( + f"stocks/candles/{resultion}/{symbol}", + params={ + "from": from_date, + "to": to_date, + }, + ): + data.pop("s") + + df = pd.DataFrame(data) + df["t"] = pd.to_datetime(df["t"], unit="s") + df.set_index("t", inplace=True) + + df.rename( + columns={ + "o": "Open", + "h": "High", + "l": "Low", + "c": "Close", + "v": "Volume", + }, + inplace=True, + ) + + self.charts[symbol.id.upper()] = df + return df + + return pd.DataFrame() diff --git a/simple-stock-bots/simple-discord-stock-bot/README.md b/simple-stock-bots/simple-discord-stock-bot/README.md index 2293aae46..0263784fa 100644 --- a/simple-stock-bots/simple-discord-stock-bot/README.md +++ b/simple-stock-bots/simple-discord-stock-bot/README.md @@ -1,176 +1,33 @@ -
-

- - Simple Discord Stock Bot -

+# Simple Discord Stock Bot -

Simple Discord Stock Bot

Buy Me A Beer [![Status](https://img.shields.io/badge/status-active-success.svg)]() [![Platform](https://img.shields.io/badge/platform-Discord-blue.svg)]() [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) +[![Author](https://img.shields.io/badge/Maintainer-Anson-blue)](https://ansonbiggs.com) -
+## Docs ---- +https://docs.simplestockbot.com/ -

Discord Bot 🤖 that provides Stock Market information. -
-

+## Usage -## 📝 Table of Contents +https://docs.simplestockbot.com/commands/ -- [About](#about) -- [How it works](#working) -- [Usage](#usage) -- [Getting Started](#getting_started) -- [Deploying your own bot](#deployment) -- [Built Using](#built_using) -- [Contributing](../CONTRIBUTING.md) -- [author](#author) -- [Acknowledgments](#acknowledgement) +## Donate -## 🧐 About +Simple Stock Bot is run entirely on donations, and costs about $420 a year to run. All donations go directly towards paying for servers, and premium market data provided by [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=repo). -This bot aims to be as simple as possible while providing all the information you need on the stock market. The motivation of this bot is to provide similar stock market functionality that the Google Assistant provided in [Google Allo](https://gcemetery.co/google-allo/) before the project was sunset. +The best way to donate is through [Buy Me A Coffee](https://www.buymeacoffee.com/Anson) which accepts Paypal or Credit card. -## 💭 How it works +If you have any questions get in [touch.](contact.md) -This bot works by using the [IEX API 2.0](https://iexcloud.io/docs/api/). Using various endpoints provided by the API, the bot can take either take commands from users or check any messages for stock symbols as detailed in [Usage](#usage). +## Other Ways to Help: -## 🎈 Usage - -### Basic Usage - -The simplest way to use the bot is just by sending a message either as a direct message or in a group chat with the bot active. The bot will search every message for text with a dollar sign followed by a stock symbol, and it will return the full name of the company and the current trading price. - -``` -$tsla -``` - -The symbols can be anywhere in the message, and you can post as many as you like so commands such as: - -``` -I wonder if $aapl is down as much as $msft is today. -``` - -would return the stock price of both Apple and Microsoft like so: - -``` -The current stock price of Microsoft Corp. is $131.4, the stock is currently up 2.8% - -The current stock price of Apple, Inc. is $190.15, the stock is currently up 2.66% -``` - -### /search `query` - -This command takes any query and searches all tickers and company names that the bot supports and returns matches. The bot uses IEX Cloud so market information is limited to what they support, which is still about ~15,000 tickers so anything in a United States exchange should have support. - -### /dividend \$ticker - -To get information about the dividend of a stock type `/dividend` followed by any text that has symbols with a dollar sign in front of them. So, the following command: - -``` -/dividend $psec -``` - -Would return information about Prospect Capitals dividend: - -``` -Prospect Capital Corp. Declares June 2019 Dividend of $0.06 Per Share -The dividend is in: 38 Days 3 Hours 53 Minutes 22 Seconds. -``` - -💡 you can also call the dividend command using /div - -### /news \$ticker - -To get the latest news about a stock symbol use `/news` followed by any text that has symbols with a dollar sign in front of them. So, the following command: - -``` -/news $psec -``` - -Would return news for Prospect Capital: - -News for PSEC: - -[Yield-Starved Investors Still Accumulating BDCs Paying More Than 10% Annually](https://cloud.iexapis.com/v1/news/article/d994b8b5-9fbf-4ceb-afbe-e6defcfc6352) - -[Assessing Main Street Capital's Results For Q1 2019 (Includes Updated Price Target And Investment Ratings Analysis)](https://cloud.iexapis.com/v1/news/article/e60899bc-5230-4388-a609-fc2b8736a7d4) - -[Fully Assessing Prospect Capital's Fiscal Q3 2019 (Includes Current Recommendation And Price Target)](https://cloud.iexapis.com/v1/news/article/08881160-72c5-4f5d-885b-1751187d24eb) - -### /info \$ticker - -To get information about a stock type `/info` followed by any text that has symbols with a dollar sign in front of them. So, the following command: - -``` -/info $psec -``` - -Would return information about Prospect Capitals: - -Company Name: [Prospect Capital Corp.](http://www.prospectstreet.com/) -Industry: Investment Managers -Sector: Finance -CEO: John Francis Barry -Description: Prospect Capital Corp. is a business development company, which engages in lending to and investing in private businesses. It also involves in generating current income and long-term capital appreciation through debt and equity investments. The company was founded on April 13, 2004 and is headquartered in New York, NY. - -## 🏁 Getting Started - -You can either choose to use the hosted version of the bot by [clicking here](https://discordapp.com/oauth2/authorize?client_id=532045200823025666&permissions=2048&scope=bot) or you can host your own bot with the instructions below. - -### Self Hosted Bot - -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system. - -### Prerequisites - -This project runs neatly in a docker container, so all that you need to run it yourself is [Docker](https://hub.docker.com/?overlay=onboarding) installed on your system. - -You will also need a Discord API key which can be obtained [here.](https://discordapp.com/developers/) - -Finally, you will need and IEX Cloud API key. They offer a free tier that should be enough for any private groups, more details [here.](https://iexcloud.io/) - -### Installing - -Once Docker is installed and you have your API keys for Discord and IEX Cloud getting the bot running on any platform is extremely easy. - -Download or clone the repository to your machine and open a terminal in the project and build the Docker container. - -``` -docker build -t simple-discord-bot . -``` - -Then run the bot using your API keys. - -``` -docker run --detach \ - -e DISCORD=DISCORD_TOKEN \ - -e IEX=IEX_TOKEN \ - simple-discord-bot -``` - -Your bot should be running! If you are new to Docker, I would recommend checking out its documentation for full control over your bot. - -## 🚀 Deploying your own bot - -I recommend Digital Ocean for small projects like this because it is straightforward to use and affordable. [Sign up with my referral code, and we both get some free hosting.](https://m.do.co/c/6b5df7ef55b6) - -## ⛏️ Built Using - -- [discord.py](https://github.com/Rapptz/discord.py) - Python Discord API Wrapper -- [Digital Ocean](https://www.digitalocean.com/) - IaaS hosting platform - -## ✍️ author - -- [Anson Biggs](https://blog.ansonbiggs.com/author/anson/) - The one and only - -## 🎉 Acknowledgements - -- Discord for having a great bot API -- IEX Cloud for offering a free tier -- Viewers like you ♥ +- Follow me on [twitter](https://twitter.com/AnsonBiggs) +- Contribute to the project on [GitLab](https://gitlab.com/simple-stock-bots) or just leave a star +- Using my referral links to host your own Bot + - [DigitalOcean](https://m.do.co/c/6b5df7ef55b6) + - [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=repo) diff --git a/simple-stock-bots/simple-discord-stock-bot/Symbol.py b/simple-stock-bots/simple-discord-stock-bot/Symbol.py index 06c25c03d..0080cc3b9 100644 --- a/simple-stock-bots/simple-discord-stock-bot/Symbol.py +++ b/simple-stock-bots/simple-discord-stock-bot/Symbol.py @@ -27,17 +27,13 @@ class Symbol: class Stock(Symbol): - """Stock Market Object. Gets data from IEX Cloud""" + """Stock Market Object. Gets data from MarketData""" - def __init__(self, symbol: pd.DataFrame) -> None: - if len(symbol) > 1: - logging.info(f"Crypto with shared id:\n\t{symbol.id}") - symbol = symbol.head(1) - - self.symbol = symbol.symbol.values[0] - self.id = symbol.id.values[0] - self.name = symbol.name.values[0] - self.tag = symbol.type_id.values[0].upper() + def __init__(self, symbol: str) -> None: + self.symbol = symbol + self.id = symbol + self.name = "$" + symbol.upper() + self.tag = "$" + symbol.lower() class Coin(Symbol): diff --git a/simple-stock-bots/simple-discord-stock-bot/bot.py b/simple-stock-bots/simple-discord-stock-bot/bot.py index 2a3716820..62ea0d265 100644 --- a/simple-stock-bots/simple-discord-stock-bot/bot.py +++ b/simple-stock-bots/simple-discord-stock-bot/bot.py @@ -3,10 +3,9 @@ import io import logging import os -import discord import mplfinance as mpf -from discord.ext import commands -from discord.flags import Intents +import nextcord +from nextcord.ext import commands from D_info import D_info from symbol_router import Router @@ -17,17 +16,17 @@ s = Router() d = D_info() -client = discord.Client() +intents = nextcord.Intents.default() -bot = commands.Bot( - command_prefix="/", description=d.help_text, intents=Intents.default() -) +client = nextcord.Client(intents=intents) +bot = commands.Bot(command_prefix="/", description=d.help_text, intents=intents) -logger = logging.getLogger("discord") -logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO -) +logger = logging.getLogger("nextcord") +logger.setLevel(logging.INFO) +handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w") +handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")) +logger.addHandler(handler) @bot.event @@ -43,9 +42,7 @@ async def status(ctx: commands): message = "" try: message = "Contact MisterBiggs#0465 if you need help.\n" - message += ( - s.status(f"Bot recieved your message in: {bot.latency*1000:.4f}ms") + "\n" - ) + message += s.status(f"Bot recieved your message in: {bot.latency*1000:.4f}ms") + "\n" except Exception as ex: logging.critical(ex) @@ -68,46 +65,6 @@ async def donate(ctx: commands): await ctx.send(d.donate_text) -@bot.command() -async def stat(ctx: commands, *, sym: str): - """Get statistics on a list of stock symbols.""" - symbols = s.find_symbols(sym) - - if symbols: - for reply in s.stat_reply(symbols): - await ctx.send(reply) - - -@bot.command() -async def dividend(ctx: commands, *, sym: str): - """Get dividend information on a stock symbol.""" - symbols = s.find_symbols(sym) - - if symbols: - for reply in s.dividend_reply(symbols): - await ctx.send(reply) - - -@bot.command() -async def news(ctx: commands, *, sym: str): - """Get recent english news on a stock symbol.""" - symbols = s.find_symbols(sym) - - if symbols: - for reply in s.news_reply(symbols): - await ctx.send(reply) - - -@bot.command() -async def info(ctx: commands, *, sym: str): - """Get information of a stock ticker.""" - symbols = s.find_symbols(sym) - - if symbols: - for reply in s.info_reply(symbols): - await ctx.send(reply[0:1900]) - - @bot.command() async def search(ctx: commands, *, query: str): """Search for a stock symbol using either symbol of company name.""" @@ -122,9 +79,7 @@ async def search(ctx: commands, *, query: str): @bot.command() async def crypto(ctx: commands, _: str): """Get the price of a cryptocurrency using in USD.""" - await ctx.send( - "Crypto now has native support. Any crypto can be called using two dollar signs: `$$eth` `$$btc` `$$doge`" - ) + await ctx.send("Crypto now has native support. Any crypto can be called using two dollar signs: `$$eth` `$$btc` `$$doge`") @bot.command() @@ -143,7 +98,6 @@ async def intra(ctx: commands, sym: str): await ctx.send("Invalid symbol please see `/help` for usage details.") return with ctx.channel.typing(): - buf = io.BytesIO() mpf.plot( df, @@ -159,7 +113,7 @@ async def intra(ctx: commands, sym: str): # Get price so theres no request lag after the image is sent price_reply = s.price_reply([symbol])[0] await ctx.send( - file=discord.File( + file=nextcord.File( buf, filename=f"{symbol.name}:intra{datetime.date.today().strftime('%S%M%d%b%Y')}.png", ), @@ -186,7 +140,6 @@ async def chart(ctx: commands, sym: str): await ctx.send("Invalid symbol please see `/help` for usage details.") return with ctx.channel.typing(): - buf = io.BytesIO() mpf.plot( df, @@ -201,7 +154,7 @@ async def chart(ctx: commands, sym: str): # Get price so theres no request lag after the image is sent price_reply = s.price_reply([symbol])[0] await ctx.send( - file=discord.File( + file=nextcord.File( buf, filename=f"{symbol.name}:1M{datetime.date.today().strftime('%d%b%Y')}.png", ), @@ -230,7 +183,6 @@ async def trending(ctx: commands): @bot.event async def on_message(message): - if message.author.id == bot.user.id: return if message.content: diff --git a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py index cc3789441..94544f4dc 100644 --- a/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py +++ b/simple-stock-bots/simple-discord-stock-bot/cg_Crypto.py @@ -1,388 +1,367 @@ -"""Class with functions for running the bot with IEX Cloud. -""" - -import logging -from datetime import datetime -from logging import critical, debug, error, info, warning -from typing import List, Optional, Tuple - -import pandas as pd -import requests as r -import schedule -from markdownify import markdownify - -from Symbol import Coin - - -class cg_Crypto: - """ - Functions for finding crypto info - """ - - vs_currency = "usd" # simple/supported_vs_currencies for list of options - - searched_symbols = {} - trending_cache = None - - def __init__(self) -> None: - """Creates a Symbol Object - - Parameters - ---------- - IEX_TOKEN : str - IEX Token - """ - self.get_symbol_list() - schedule.every().day.do(self.get_symbol_list) - - def get(self, endpoint, params: dict = {}, timeout=10) -> dict: - - url = "https://api.coingecko.com/api/v3" + endpoint - resp = r.get(url, params=params, timeout=timeout) - # Make sure API returned a proper status code - try: - resp.raise_for_status() - except r.exceptions.HTTPError as e: - logging.error(e) - return {} - - # Make sure API returned valid JSON - try: - resp_json = resp.json() - return resp_json - except r.exceptions.JSONDecodeError as e: - logging.error(e) - return {} - - def symbol_id(self, symbol) -> str: - try: - return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[ - 0 - ] - except KeyError: - return "" - - def get_symbol_list( - self, return_df=False - ) -> Optional[Tuple[pd.DataFrame, datetime]]: - - raw_symbols = self.get("/coins/list") - symbols = pd.DataFrame(data=raw_symbols) - - # Removes all binance-peg symbols - symbols = symbols[~symbols["id"].str.contains("binance-peg")] - - symbols["description"] = ( - "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"] - ) - symbols = symbols[["id", "symbol", "name", "description"]] - symbols["type_id"] = "$$" + symbols["symbol"] - - self.symbol_list = symbols - if return_df: - return symbols, datetime.now() - - def status(self) -> str: - """Checks CoinGecko /ping endpoint for API issues. - - Returns - ------- - str - Human readable text on status of CoinGecko API - """ - status = r.get( - "https://api.coingecko.com/api/v3/ping", - timeout=5, - ) - - try: - status.raise_for_status() - return f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} Seconds." - except: - return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." - - def price_reply(self, coin: Coin) -> str: - """Returns current market price or after hours if its available for a given coin symbol. - - Parameters - ---------- - symbols : list - List of coin symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable - markdown formatted string of the symbols price and movement. - """ - - if resp := self.get( - "/simple/price", - params={ - "ids": coin.id, - "vs_currencies": self.vs_currency, - "include_24hr_change": "true", - }, - ): - try: - data = resp[coin.id] - - price = data[self.vs_currency] - change = data[self.vs_currency + "_24h_change"] - if change is None: - change = 0 - except KeyError: - return f"{coin.id} returned an error." - - message = f"The current price of {coin.name} is $**{price:,}**" - - # Determine wording of change text - if change > 0: - message += f", the coin is currently **up {change:.3f}%** for today" - elif change < 0: - message += f", the coin is currently **down {change:.3f}%** for today" - else: - message += ", the coin hasn't shown any movement today." - - else: - message = f"The price for {coin.name} is not available. If you suspect this is an error run `/status`" - - return message - - def intra_reply(self, symbol: Coin) -> pd.DataFrame: - """Returns price data for a symbol since the last market open. - - Parameters - ---------- - symbol : str - Stock symbol. - - Returns - ------- - pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. - """ - - if resp := self.get( - f"/coins/{symbol.id}/ohlc", - params={"vs_currency": self.vs_currency, "days": 1}, - ): - df = pd.DataFrame( - resp, columns=["Date", "Open", "High", "Low", "Close"] - ).dropna() - df["Date"] = pd.to_datetime(df["Date"], unit="ms") - df = df.set_index("Date") - return df - - return pd.DataFrame() - - def chart_reply(self, symbol: Coin) -> pd.DataFrame: - """Returns price data for a symbol of the past month up until the previous trading days close. - Also caches multiple requests made in the same day. - - Parameters - ---------- - symbol : str - Stock symbol. - - Returns - ------- - pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. - """ - - if resp := self.get( - f"/coins/{symbol.id}/ohlc", - params={"vs_currency": self.vs_currency, "days": 30}, - ): - df = pd.DataFrame( - resp, columns=["Date", "Open", "High", "Low", "Close"] - ).dropna() - df["Date"] = pd.to_datetime(df["Date"], unit="ms") - df = df.set_index("Date") - return df - - return pd.DataFrame() - - def stat_reply(self, symbol: Coin) -> str: - """Gathers key statistics on coin. Mostly just CoinGecko scores. - - Parameters - ---------- - symbol : Coin - - Returns - ------- - str - Preformatted markdown. - """ - - if data := self.get( - f"/coins/{symbol.id}", - params={ - "localization": "false", - }, - ): - - return f""" - [{data['name']}]({data['links']['homepage'][0]}) Statistics: - Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,} - Market Cap Ranking: {data.get('market_cap_rank',"Not Available")} - CoinGecko Scores: - Overall: {data.get('coingecko_score','Not Available')} - Development: {data.get('developer_score','Not Available')} - Community: {data.get('community_score','Not Available')} - Public Interest: {data.get('public_interest_score','Not Available')} - """ - else: - return f"{symbol.symbol} returned an error." - - def cap_reply(self, coin: Coin) -> str: - """Gets market cap for Coin - - Parameters - ---------- - coin : Coin - - Returns - ------- - str - Preformatted markdown. - """ - - if resp := self.get( - f"/simple/price", - params={ - "ids": coin.id, - "vs_currencies": self.vs_currency, - "include_market_cap": "true", - }, - ): - debug(resp) - try: - data = resp[coin.id] - - price = data[self.vs_currency] - cap = data[self.vs_currency + "_market_cap"] - except KeyError: - return f"{coin.id} returned an error." - - if cap == 0: - return f"The market cap for {coin.name} is not available for unknown reasons." - - message = f"The current price of {coin.name} is $**{price:,}** and its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}" - - else: - message = f"The Coin: {coin.name} was not found or returned and error." - - return message - - def info_reply(self, symbol: Coin) -> str: - """Gets coin description - - Parameters - ---------- - symbol : Coin - - Returns - ------- - str - Preformatted markdown. - """ - - if data := self.get( - f"/coins/{symbol.id}", - params={"localization": "false"}, - ): - try: - return markdownify(data["description"]["en"]) - except KeyError: - return f"{symbol} does not have a description available." - - return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - - def spark_reply(self, symbol: Coin) -> str: - change = self.get( - f"/simple/price", - params={ - "ids": symbol.id, - "vs_currencies": self.vs_currency, - "include_24hr_change": "true", - }, - )[symbol.id]["usd_24h_change"] - - return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%" - - def trending(self) -> list[str]: - """Gets current coins trending on coingecko - - Returns - ------- - list[str] - list of $$ID: NAME, CHANGE% - """ - - coins = self.get("/search/trending") - try: - trending = [] - for coin in coins["coins"]: - c = coin["item"] - - sym = c["symbol"].upper() - name = c["name"] - change = self.get( - f"/simple/price", - params={ - "ids": c["id"], - "vs_currencies": self.vs_currency, - "include_24hr_change": "true", - }, - )[c["id"]]["usd_24h_change"] - - msg = f"`$${sym}`: {name}, {change:.2f}%" - - trending.append(msg) - - except Exception as e: - logging.warning(e) - return self.trending_cache - - self.trending_cache = trending - return trending - - def batch_price(self, coins: list[Coin]) -> list[str]: - """Gets price of a list of coins all in one API call - - Parameters - ---------- - coins : list[Coin] - - Returns - ------- - list[str] - returns preformatted list of strings detailing price movement of each coin passed in. - """ - query = ",".join([c.id for c in coins]) - - prices = self.get( - f"/simple/price", - params={ - "ids": query, - "vs_currencies": self.vs_currency, - "include_24hr_change": "true", - }, - ) - - replies = [] - for coin in coins: - if coin.id in prices: - p = prices[coin.id] - - if p.get("usd_24h_change") is None: - p["usd_24h_change"] = 0 - - replies.append( - f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours." - ) - - return replies +import logging +from typing import List + +import pandas as pd +import requests as r +import schedule +from markdownify import markdownify + +from Symbol import Coin + +log = logging.getLogger(__name__) + + +class cg_Crypto: + """ + Functions for finding crypto info + """ + + vs_currency = "usd" # simple/supported_vs_currencies for list of options + + trending_cache: List[str] = [] + + def __init__(self) -> None: + self.get_symbol_list() + schedule.every().day.do(self.get_symbol_list) + + def get(self, endpoint, params: dict = {}, timeout=10) -> dict: + url = "https://api.coingecko.com/api/v3" + endpoint + resp = r.get(url, params=params, timeout=timeout) + # Make sure API returned a proper status code + try: + resp.raise_for_status() + except r.exceptions.HTTPError as e: + log.error(e) + return {} + + # Make sure API returned valid JSON + try: + resp_json = resp.json() + return resp_json + except r.exceptions.JSONDecodeError as e: + log.error(e) + return {} + + def symbol_id(self, symbol) -> str: + try: + return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[0] + except KeyError: + return "" + + def get_symbol_list(self): + raw_symbols = self.get("/coins/list") + symbols = pd.DataFrame(data=raw_symbols) + + # Removes all binance-peg symbols + symbols = symbols[~symbols["id"].str.contains("binance-peg")] + + symbols["description"] = "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"] + symbols = symbols[["id", "symbol", "name", "description"]] + symbols["type_id"] = "$$" + symbols["symbol"] + + self.symbol_list = symbols + + def status(self) -> str: + """Checks CoinGecko /ping endpoint for API issues. + + Returns + ------- + str + Human readable text on status of CoinGecko API + """ + status = r.get( + "https://api.coingecko.com/api/v3/ping", + timeout=5, + ) + + try: + status.raise_for_status() + return ( + f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} Seconds." + ) + except r.HTTPError: + return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." + + def price_reply(self, coin: Coin) -> str: + """Returns current market price or after hours if its available for a given coin symbol. + + Parameters + ---------- + symbols : list + List of coin symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + markdown formatted string of the symbols price and movement. + """ + + if resp := self.get( + "/simple/price", + params={ + "ids": coin.id, + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + ): + try: + data = resp[coin.id] + + price = data[self.vs_currency] + change = data[self.vs_currency + "_24h_change"] + if change is None: + change = 0 + except KeyError: + return f"{coin.id} returned an error." + + message = f"The current price of {coin.name} is $**{price:,}**" + + # Determine wording of change text + if change > 0: + message += f", the coin is currently **up {change:.3f}%** for today" + elif change < 0: + message += f", the coin is currently **down {change:.3f}%** for today" + else: + message += ", the coin hasn't shown any movement today." + + else: + message = f"The price for {coin.name} is not available. If you suspect this is an error run `/status`" + + return message + + def intra_reply(self, symbol: Coin) -> pd.DataFrame: + """Returns price data for a symbol since the last market open. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + + if resp := self.get( + f"/coins/{symbol.id}/ohlc", + params={"vs_currency": self.vs_currency, "days": 1}, + ): + df = pd.DataFrame(resp, columns=["Date", "Open", "High", "Low", "Close"]).dropna() + df["Date"] = pd.to_datetime(df["Date"], unit="ms") + df = df.set_index("Date") + return df + + return pd.DataFrame() + + def chart_reply(self, symbol: Coin) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + + if resp := self.get( + f"/coins/{symbol.id}/ohlc", + params={"vs_currency": self.vs_currency, "days": 30}, + ): + df = pd.DataFrame(resp, columns=["Date", "Open", "High", "Low", "Close"]).dropna() + df["Date"] = pd.to_datetime(df["Date"], unit="ms") + df = df.set_index("Date") + return df + + return pd.DataFrame() + + def stat_reply(self, symbol: Coin) -> str: + """Gathers key statistics on coin. Mostly just CoinGecko scores. + + Parameters + ---------- + symbol : Coin + + Returns + ------- + str + Preformatted markdown. + """ + + if data := self.get( + f"/coins/{symbol.id}", + params={ + "localization": "false", + }, + ): + return f""" + [{data['name']}]({data['links']['homepage'][0]}) Statistics: + Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,} + Market Cap Ranking: {data.get('market_cap_rank',"Not Available")} + CoinGecko Scores: + Overall: {data.get('coingecko_score','Not Available')} + Development: {data.get('developer_score','Not Available')} + Community: {data.get('community_score','Not Available')} + Public Interest: {data.get('public_interest_score','Not Available')} + """ + else: + return f"{symbol.symbol} returned an error." + + def cap_reply(self, coin: Coin) -> str: + """Gets market cap for Coin + + Parameters + ---------- + coin : Coin + + Returns + ------- + str + Preformatted markdown. + """ + + if resp := self.get( + "/simple/price", + params={ + "ids": coin.id, + "vs_currencies": self.vs_currency, + "include_market_cap": "true", + }, + ): + log.debug(resp) + try: + data = resp[coin.id] + + price = data[self.vs_currency] + cap = data[self.vs_currency + "_market_cap"] + except KeyError: + return f"{coin.id} returned an error." + + if cap == 0: + return f"The market cap for {coin.name} is not available for unknown reasons." + + message = ( + f"The current price of {coin.name} is $**{price:,}** and" + + " its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}" + ) + + else: + message = f"The Coin: {coin.name} was not found or returned and error." + + return message + + def info_reply(self, symbol: Coin) -> str: + """Gets coin description + + Parameters + ---------- + symbol : Coin + + Returns + ------- + str + Preformatted markdown. + """ + + if data := self.get( + f"/coins/{symbol.id}", + params={"localization": "false"}, + ): + try: + return markdownify(data["description"]["en"]) + except KeyError: + return f"{symbol} does not have a description available." + + return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + + def spark_reply(self, symbol: Coin) -> str: + change = self.get( + "/simple/price", + params={ + "ids": symbol.id, + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + )[symbol.id]["usd_24h_change"] + + return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%" + + def trending(self) -> list[str]: + """Gets current coins trending on coingecko + + Returns + ------- + list[str] + list of $$ID: NAME, CHANGE% + """ + + coins = self.get("/search/trending") + try: + trending = [] + for coin in coins["coins"]: + c = coin["item"] + + sym = c["symbol"].upper() + name = c["name"] + change = self.get( + "/simple/price", + params={ + "ids": c["id"], + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + )[c["id"]]["usd_24h_change"] + + msg = f"`$${sym}`: {name}, {change:.2f}%" + + trending.append(msg) + + except Exception as e: + log.warning(e) + return self.trending_cache + + self.trending_cache = trending + return trending + + def batch_price(self, coins: list[Coin]) -> list[str]: + """Gets price of a list of coins all in one API call + + Parameters + ---------- + coins : list[Coin] + + Returns + ------- + list[str] + returns preformatted list of strings detailing price movement of each coin passed in. + """ + query = ",".join([c.id for c in coins]) + + prices = self.get( + "/simple/price", + params={ + "ids": query, + "vs_currencies": self.vs_currency, + "include_24hr_change": "true", + }, + ) + + replies = [] + for coin in coins: + if coin.id in prices: + p = prices[coin.id] + + if p.get("usd_24h_change") is None: + p["usd_24h_change"] = 0 + + replies.append( + f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours." + ) + + return replies diff --git a/simple-stock-bots/simple-discord-stock-bot/dev-reqs.txt b/simple-stock-bots/simple-discord-stock-bot/dev-reqs.txt new file mode 100644 index 000000000..c12235826 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/dev-reqs.txt @@ -0,0 +1,8 @@ +-r requirements.txt +black==23.3.0 +flake8==5.0.4 +Flake8-pyproject==1.2.3 +pylama==8.4.1 +mypy==1.2.0 +types-cachetools==5.3.0.5 +types-pytz==2023.3.0.0 \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/pyproject.toml b/simple-stock-bots/simple-discord-stock-bot/pyproject.toml new file mode 100644 index 000000000..e87df56b3 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 130 + +[tool.flake8] +max-line-length = 130 + +[tool.pycodestyle] +max_line_length = 130 \ No newline at end of file diff --git a/simple-stock-bots/simple-discord-stock-bot/requirements.txt b/simple-stock-bots/simple-discord-stock-bot/requirements.txt index bf45fe5fa..70488b8b9 100644 --- a/simple-stock-bots/simple-discord-stock-bot/requirements.txt +++ b/simple-stock-bots/simple-discord-stock-bot/requirements.txt @@ -1,6 +1,6 @@ -nextcord==2.0.0a5 +nextcord==2.4.2 requests==2.25.1 -pandas==1.2.1 +pandas==2.0.0 schedule==1.0.0 mplfinance==0.12.7a5 markdownify==0.6.5 diff --git a/simple-stock-bots/simple-discord-stock-bot/symbol_router.py b/simple-stock-bots/simple-discord-stock-bot/symbol_router.py index d61ce66b0..7c5544c36 100644 --- a/simple-stock-bots/simple-discord-stock-bot/symbol_router.py +++ b/simple-stock-bots/simple-discord-stock-bot/symbol_router.py @@ -5,24 +5,27 @@ import datetime import logging import random import re -from logging import critical, debug, error, info, warning import pandas as pd import schedule from cachetools import TTLCache, cached from cg_Crypto import cg_Crypto -from IEX_Symbol import IEX_Symbol +from MarketData import MarketData from Symbol import Coin, Stock, Symbol +from typing import Dict + +log = logging.getLogger(__name__) + class Router: STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})" CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})" - trending_count = {} + trending_count: Dict[str, float] = {} def __init__(self): - self.stock = IEX_Symbol() + self.stock = MarketData() self.crypto = cg_Crypto() schedule.every().hour.do(self.trending_decay) @@ -43,9 +46,9 @@ class Router: t_copy.pop(dead) self.trending_count = t_copy.copy() - info("Decayed trending symbols.") + log.info("Decayed trending symbols.") - def find_symbols(self, text: str, *, trending_weight: int = 1) -> list[Symbol]: + def find_symbols(self, text: str, *, trending_weight: int = 1) -> list[Stock | Symbol]: """Finds stock tickers starting with a dollar sign, and cryptocurrencies with two dollar signs in a blob of text and returns them in a list. @@ -61,37 +64,25 @@ class Router: """ schedule.run_pending() - symbols = [] + symbols: list[Symbol] = [] stocks = set(re.findall(self.STOCK_REGEX, text)) for stock in stocks: - sym = self.stock.symbol_list[ - self.stock.symbol_list["symbol"].str.fullmatch(stock, case=False) - ] - if ~sym.empty: - print(sym) - symbols.append(Stock(sym)) - else: - info(f"{stock} is not in list of stocks") + # Market data lacks tools to check if a symbol is valid. + symbols.append(Stock(stock)) coins = set(re.findall(self.CRYPTO_REGEX, text)) for coin in coins: - sym = self.crypto.symbol_list[ - self.crypto.symbol_list["symbol"].str.fullmatch( - coin.lower(), case=False - ) - ] - if ~sym.empty: - symbols.append(Coin(sym)) + sym = self.crypto.symbol_list[self.crypto.symbol_list["symbol"].str.fullmatch(coin.lower(), case=False)] + if sym.empty: + log.info(f"{coin} is not in list of coins") else: - info(f"{coin} is not in list of coins") + symbols.append(Coin(sym)) if symbols: - info(symbols) for symbol in symbols: - self.trending_count[symbol.tag] = ( - self.trending_count.get(symbol.tag, 0) + trending_weight - ) + self.trending_count[symbol.tag] = self.trending_count.get(symbol.tag, 0) + trending_weight + log.debug(self.trending_count) - return symbols + return symbols def status(self, bot_resp) -> str: """Checks for any issues with APIs. @@ -113,7 +104,7 @@ class Router: {self.crypto.status()} """ - warning(stats) + log.warning(stats) return stats @@ -134,9 +125,9 @@ class Router: df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) - df = df[ - df["description"].str.contains(search, regex=False, case=False) - ].sort_values(by="type_id", key=lambda x: x.str.len()) + df = df[df["description"].str.contains(search, regex=False, case=False)].sort_values( + by="type_id", key=lambda x: x.str.len() + ) symbols = df.head(matches) symbols["price_reply"] = symbols["type_id"].apply( @@ -162,67 +153,13 @@ class Router: replies = [] for symbol in symbols: - info(symbol) + log.info(symbol) if isinstance(symbol, Stock): replies.append(self.stock.price_reply(symbol)) elif isinstance(symbol, Coin): replies.append(self.crypto.price_reply(symbol)) else: - info(f"{symbol} is not a Stock or Coin") - - return replies - - def dividend_reply(self, symbols: list) -> list[str]: - """Returns the most recent, or next dividend date for a stock symbol. - - Parameters - ---------- - symbols : list - List of stock symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable - formatted string of the symbols div dates. - """ - replies = [] - for symbol in symbols: - if isinstance(symbol, Stock): - replies.append(self.stock.dividend_reply(symbol)) - elif isinstance(symbol, Coin): - replies.append("Cryptocurrencies do no have Dividends.") - else: - debug(f"{symbol} is not a Stock or Coin") - - return replies - - def news_reply(self, symbols: list) -> list[str]: - """Gets recent english news on stock symbols. - - Parameters - ---------- - symbols : list - List of stock symbols. - - Returns - ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human - readable markdown formatted string of the symbols news. - """ - replies = [] - - for symbol in symbols: - if isinstance(symbol, Stock): - replies.append(self.stock.news_reply(symbol)) - elif isinstance(symbol, Coin): - # replies.append(self.crypto.news_reply(symbol)) - replies.append( - "News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs" - ) - else: - debug(f"{symbol} is not a Stock or Coin") + log.info(f"{symbol} is not a Stock or Coin") return replies @@ -248,7 +185,7 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.info_reply(symbol)) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") return replies @@ -272,7 +209,7 @@ class Router: elif isinstance(symbol, Coin): return self.crypto.intra_reply(symbol) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") return pd.DataFrame() def chart_reply(self, symbol: Symbol) -> pd.DataFrame: @@ -295,7 +232,7 @@ class Router: elif isinstance(symbol, Coin): return self.crypto.chart_reply(symbol) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") return pd.DataFrame() def stat_reply(self, symbols: list[Symbol]) -> list[str]: @@ -320,7 +257,7 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.stat_reply(symbol)) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") return replies @@ -346,7 +283,7 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.cap_reply(symbol)) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") return replies @@ -371,7 +308,7 @@ class Router: elif isinstance(symbol, Coin): replies.append(self.crypto.spark_reply(symbol)) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") return replies @@ -385,29 +322,21 @@ class Router: List of preformatted strings to be sent to user. """ - stocks = self.stock.trending() + # stocks = self.stock.trending() coins = self.crypto.trending() reply = "" + log.warning(self.trending_count) if self.trending_count: reply += "🔥Trending on the Stock Bot:\n`" reply += "━" * len("Trending on the Stock Bot:") + "`\n" - sorted_trending = [ - s[0] - for s in sorted(self.trending_count.items(), key=lambda item: item[1]) - ][::-1][0:5] - + sorted_trending = [s[0] for s in sorted(self.trending_count.items(), key=lambda item: item[1])][::-1][0:5] + log.warning(sorted_trending) for t in sorted_trending: reply += self.spark_reply(self.find_symbols(t))[0] + "\n" - if stocks: - reply += "\n\n💵Trending Stocks:\n`" - reply += "━" * len("Trending Stocks:") + "`\n" - for stock in stocks: - reply += stock + "\n" - if coins: reply += "\n\n🦎Trending Crypto:\n`" reply += "━" * len("Trending Crypto:") + "`\n" @@ -420,18 +349,12 @@ class Router: if reply: return reply else: - warning("Failed to collect trending data.") + log.warning("Failed to collect trending data.") return "Trending data is not currently available." def random_pick(self) -> str: - - choice = random.choice( - list(self.stock.symbol_list["description"]) - + list(self.crypto.symbol_list["description"]) - ) - hold = ( - datetime.date.today() + datetime.timedelta(random.randint(1, 365)) - ).strftime("%b %d, %Y") + choice = random.choice(list(self.stock.symbol_list["description"]) + list(self.crypto.symbol_list["description"])) + hold = (datetime.date.today() + datetime.timedelta(random.randint(1, 365))).strftime("%b %d, %Y") return f"{choice}\nBuy and hold until: {hold}" @@ -459,10 +382,9 @@ class Router: elif isinstance(symbol, Coin): coins.append(symbol) else: - debug(f"{symbol} is not a Stock or Coin") + log.debug(f"{symbol} is not a Stock or Coin") if stocks: - # IEX batch endpoint doesnt seem to be working right now for stock in stocks: replies.append(self.stock.price_reply(stock)) if coins: diff --git a/simple-stock-bots/simple-discord-stock-bot/tests.py b/simple-stock-bots/simple-discord-stock-bot/tests.py new file mode 100644 index 000000000..821276661 --- /dev/null +++ b/simple-stock-bots/simple-discord-stock-bot/tests.py @@ -0,0 +1,33 @@ +import keyboard +import time + + +tests = """$$xno +/info $tsla +/info $$btc +/news $tsla +/news $$btc +/stat $tsla +/stat $$btc +/cap $tsla +/cap $$btc +/dividend $tsla +/dividend $msft +/dividend $$btc +/intra $tsla +/intra $$btc +/chart $tsla +/chart $$btc +/help +/trending""".split( + "\n" +) + +print("press enter to start") +keyboard.wait("enter") + +for test in tests: + print(test) + keyboard.write(test) + time.sleep(1) + keyboard.press_and_release("enter")