├── .gitignore ├── requirements.txt ├── example.env ├── example.py ├── LICENSE.md ├── scrapper.py ├── paper.py ├── signaler.py ├── communication.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .env 3 | *.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==0.13.0 2 | twilio==6.43.0 3 | yagmail==0.11.224 -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # 1. Change the details below. 2 | # 2. Save the file "example.env" as ".env" in current directory (/bursapy). 3 | 4 | 5 | # Your mail information: 6 | SENDER_MAIL = 'yourmail@maildomain.com' # Your mail 7 | MAIL_PASS = 'your mail password' 8 | SMTP_DOMAIN = 'smtp.gmail.com' 9 | 10 | RECEIVER_MAIL = 'mail@domain.com' # This can be same mail as sender's! 11 | 12 | 13 | # Twilio API keys and phone number: 14 | TWILIO_SID = 'your_twilio_sid' 15 | 16 | TWILIO_TOKEN = 'your_twilio_token' 17 | TWILIO_PHONE = '+11111111111' 18 | 19 | # receiver's phone number 20 | RECEIVER_PHONE = '+11111111111' # Your phone number -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from time import sleep 3 | import signaler 4 | from paper import Paper 5 | 6 | 7 | DAY_IN_SECS = 86400 8 | TODAY = datetime.weekday(datetime.today()) 9 | FRI = 4 10 | SAT = 5 11 | SIGNAL_TIME = 11 12 | TIME_NOW = datetime.now().hour 13 | 14 | portfolio = set() 15 | 16 | portfolio.add(Paper('777037')) 17 | portfolio.add(Paper('1123777')) 18 | portfolio.add(Paper('1104249')) 19 | portfolio.add(Paper('1157833')) 20 | portfolio.add(Paper('103010')) 21 | portfolio.add(Paper('1161264')) 22 | 23 | while True: 24 | if TODAY != FRI and TODAY != SAT: 25 | if TIME_NOW == SIGNAL_TIME: 26 | for paper in portfolio: 27 | signaler.send_percentage_change(paper, 0.1, 'mail') 28 | signaler.send_percentage_change(paper, 0.1, 'wapp') 29 | sleep(DAY_IN_SECS) 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nedlir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scrapper.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | from urllib.request import urlopen 4 | 5 | 6 | FETCH_URL = "https://www.bizportal.co.il/forex/quote/ajaxrequests/paperdatagraphjson?" 7 | DICT_PAPER_NAME = 'paperName' 8 | DICT_POINTS = 'points' 9 | DICT_DATE = 'D_p' 10 | TODAY = datetime.weekday(datetime.today()) 11 | FRI = 4 12 | SAT = 5 13 | 14 | 15 | def fetch_paper_data(paper_id=''): 16 | period = 'yearly' # Period must be set to: 5Years, yearly or daily 17 | url = FETCH_URL + f"period={period}&paperID={paper_id}" 18 | 19 | with urlopen(url) as response: 20 | source = response.read() 21 | temp_dict = json.loads(source) 22 | 23 | if TODAY != FRI and TODAY != SAT: 24 | date_today = datetime.today().strftime('%d/%m/%Y') 25 | today = fetch_daily_data(date_today, paper_id) 26 | temp_dict.get(DICT_POINTS).insert(0, today) 27 | 28 | return temp_dict.get(DICT_POINTS), temp_dict.get(DICT_PAPER_NAME) 29 | 30 | 31 | def fetch_daily_data(date_today='00/00/0000', paper_id=''): 32 | period = 'daily' 33 | url = FETCH_URL + f"period={period}&paperID={paper_id}" 34 | 35 | with urlopen(url) as response: 36 | source = response.read() 37 | temp_dict = json.loads(source) 38 | 39 | # Initializes today's points with latest trading value 40 | today = temp_dict.get(DICT_POINTS)[-1] 41 | today.update({DICT_DATE: date_today}) 42 | 43 | return today -------------------------------------------------------------------------------- /paper.py: -------------------------------------------------------------------------------- 1 | import scrapper 2 | 3 | 4 | DICT_PAPER_VALUE = 'C_p' 5 | 6 | 7 | class Paper(): 8 | 9 | def __init__(self, paper_id: str): 10 | if paper_id.isnumeric(): 11 | self._id = paper_id 12 | else: 13 | self._id = '11111026' # S&P 500 14 | 15 | self._points, self._name = scrapper.fetch_paper_data(self._id) 16 | 17 | def get_daily_precetage_change(self): 18 | val_today = self._points[0].get(DICT_PAPER_VALUE) 19 | val_yesterday = self._points[1].get(DICT_PAPER_VALUE) 20 | 21 | return round(100 - ((val_today / val_yesterday) * 100), 6) 22 | 23 | def get_daily_rate(self): 24 | daily_value = self._points[0].get(DICT_PAPER_VALUE) 25 | 26 | return daily_value 27 | 28 | def update_points_data(self): 29 | self._points = scrapper.fetch_paper_data(self.id)[0] 30 | 31 | def __str__(self): 32 | return self._id + ' - ' + self._name 33 | 34 | def __repr__(self): 35 | return f'Paper({self._id}, {self._name})' 36 | 37 | @property 38 | def name(self): 39 | return self._name 40 | 41 | @name.setter 42 | def name(self, name=''): 43 | if name.isprintable() and name.isalpha(): 44 | self._name = name 45 | else: 46 | raise Exception("The paper's name must be valid letters") 47 | 48 | @property 49 | def id(self): 50 | return self._id 51 | 52 | @property 53 | def points(self): 54 | return self._points -------------------------------------------------------------------------------- /signaler.py: -------------------------------------------------------------------------------- 1 | import communication 2 | from paper import Paper 3 | 4 | 5 | def send_communication(subject: str, body: str, communicate: str): 6 | if communicate == 'gmail': 7 | communication.send_gmail(subject, body) 8 | elif communicate == 'mail': 9 | communication.send_mail(subject, body) 10 | elif communicate == 'wapp': 11 | communication.send_whatsapp(body) 12 | elif communicate == 'sms': 13 | communication.send_sms(body) 14 | 15 | 16 | def send_percentage_change(paper, target_percentage: float, 17 | communicate='gmail, mail, sms or wapp'): 18 | paper.update_points_data() 19 | change = paper.get_daily_precetage_change() 20 | 21 | if target_percentage < 0: 22 | subject, body = communication.message_percentage( 23 | paper.name, target_percentage, change, 'decline') 24 | 25 | if change <= target_percentage: 26 | send_communication(subject, body, communicate) 27 | 28 | elif target_percentage > 0: 29 | subject, body = communication.message_percentage( 30 | paper.name, target_percentage, change, 'rise') 31 | 32 | if change >= target_percentage: 33 | send_communication(subject, body, communicate) 34 | 35 | else: # Update daily regardless of change 36 | subject, body = communication.message_percentage( 37 | paper.name, target_percentage, change, 'change') 38 | 39 | send_communication(subject, body, communicate) 40 | 41 | 42 | def send_rate_change(paper, target_rate: int, is_above=True, 43 | communicate='gmail, mail, sms or wapp'): 44 | paper.update_points_data() 45 | daily_rate = paper.get_daily_rate() 46 | 47 | if is_above and daily_rate >= target_rate: 48 | subject, body = communication.message_rate( 49 | paper.name, target_rate, daily_rate, 'above') 50 | 51 | send_communication(subject, body, communicate) 52 | 53 | elif not is_above and daily_rate <= target_rate: 54 | subject, body = communication.message_rate( 55 | paper.name, target_rate, daily_rate, 'below') 56 | 57 | send_communication(subject, body, communicate) -------------------------------------------------------------------------------- /communication.py: -------------------------------------------------------------------------------- 1 | import os 2 | import smtplib 3 | from email.message import EmailMessage 4 | from dotenv import load_dotenv 5 | import yagmail 6 | from twilio.rest import Client 7 | 8 | 9 | load_dotenv() 10 | 11 | sender_mail = os.getenv('SENDER_MAIL') 12 | sender_password = os.getenv('MAIL_PASS') 13 | receiver_mail = os.getenv('RECEIVER_MAIL') 14 | twilio_sid = os.getenv('TWILIO_SID') 15 | twilio_token = os.getenv('TWILIO_TOKEN') 16 | sender_phone = os.getenv('TWILIO_PHONE') 17 | receiver_phone = os.getenv('RECEIVER_PHONE') 18 | smtp_domain = os.getenv('SMTP_DOMAIN') 19 | 20 | 21 | def message_percentage(paper_name: str, target: float, 22 | change: float, status='decrease or rise'): 23 | 24 | subject = f"Update! {paper_name} has shown {status} in it's value!" 25 | body = f"{paper_name} has shown {status} of {change}% in it's value. "\ 26 | + f"Target change was: {target}%" 27 | 28 | return subject, body 29 | 30 | 31 | def message_rate(paper_name: str, target: int, 32 | change: int, status='below or above'): 33 | 34 | subject = f"Update! {paper_name} has shown change in it's rate value!" 35 | body = f"{paper_name}'s current rate is: {change}. "\ 36 | + f"Target rate was {status} {target} points." 37 | 38 | return subject, body 39 | 40 | 41 | def send_gmail(subject: str, body: str): 42 | with yagmail.SMTP(user=sender_mail, password=sender_password) as yag: 43 | try: 44 | yag.send(receiver_mail, subject, body) 45 | except: 46 | print("Couldn't send Mail using yagmail.") 47 | 48 | 49 | def send_mail(subject: str, body: str): 50 | 51 | msg = EmailMessage() 52 | msg['Subject'] = subject 53 | msg['From'] = sender_mail 54 | msg['To'] = receiver_mail 55 | 56 | msg.set_content(body) 57 | 58 | with smtplib.SMTP_SSL(smtp_domain, 465) as smtp: 59 | try: 60 | smtp.login(sender_mail, sender_password) 61 | smtp.send_message(msg) 62 | except: 63 | print("Couldn't send Mail using smtplib.") 64 | 65 | 66 | def send_sms(message: str): 67 | client = Client(twilio_sid, twilio_token) 68 | try: 69 | client.messages.create(body=message, 70 | from_=sender_phone, 71 | to=receiver_phone) 72 | except: 73 | print("Couldn't send SMS using Twilio's API.") 74 | 75 | 76 | def send_whatsapp(message: str): 77 | client = Client(twilio_sid, twilio_token) 78 | try: 79 | client.messages.create(body=message, 80 | from_='whatsapp:' + sender_phone, 81 | to='whatsapp:' + receiver_phone) 82 | except: 83 | print("Couldn't send WhatsApp using Twilio's API.") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :chart_with_downwards_trend: Welcome to Bursa Signaler! :chart_with_upwards_trend: 2 | 3 | Bursa Signaler is a simple python script utilizing [Bizportal](https://www.bizportal.co.il)'s API. The script is designed to assist those who are interested in following Israeli stock market papers in their portfolio efortlessly. 4 | 5 | Bursa Signaler provides you updates regarding a stock drop or rise in value through favourite communication method: WhatsApp, SMS or Email. 6 | 7 | Bursa Signaler lets you choose a percentage or point rate targets of the stock you follow and informs you by the chosen method whether the target change occured. This allows you to invest passively without constantly checking your portfolio. This practice has been suggested by [behavioural economy researchers](https://en.wikipedia.org/wiki/Robert_J._Shiller) as an healthy practice. 8 | 9 | Bursa Signaler gives you the chance to capitalize on [flash crashes](https://en.wikipedia.org/wiki/2010_flash_crash) or any crash in the stock market as a long term investor. 10 | 11 | Please refer to disclaimer in the next section before utilizing the script. 12 | 13 | ## Disclaimer :page_with_curl: 14 | 15 | >The information contained herein is provided for informational purposes only, is not comprehensive, does not contain important disclosures and risk factors associated with investments, and is subject to change without notice. 16 | >The author of this code (from now on: The author) is not responsible for the accuracy, completeness or lack thereof of information, nor has the author verified information from third parties which may be relied upon. 17 | >The information does not take into account the particular investment objectives or financial circumstances of any specific person or organization which may view it. 18 | >The author is not a registered investment advisor and does not represent the information as a recommendation for users to buy or sell the securities under discussion. 19 | >Nothing contained within may be considered an offer or a solicitation to purchase or sell any particular financial instrument. 20 | >All liability for the content of this information, including any omissions, inaccuracies, errors, or misstatements is expressly disclaimed. Always complete your own due diligence. Before making any investment, investors are advised to review such investment thoroughly and carefully with their financial, legal and tax advisors to determine whether it is suitable for them. 21 | >Investing in the mentioned securities is dangerous and may cause a loss of the entire invested capital. 22 | >If you act according to information presented on this page and by using the code you bear sole responsibility for the results of these actions, the author shall not be held responsible for any such action whatsoever. 23 | 24 | Alright, now that we are done with the necessary CYA, let's go straight over to the usage instructions of the script. 25 | 26 | ## Instructions :book: 27 | 28 | Please install required packages and follow the instructions. 29 | Check [example.py](example.py) for an example of how this script may be implemented. 30 | 31 | ### Requirements :clipboard: 32 | - Python 3.8 is the version the script was built for, it may work on earlier versions as well. 33 | - Sms and WhatsApp communication require account at [Twilio](https://www.twilio.com). 34 | - Install missing packages from [requirements.txt](requirements.txt) file. 35 | 36 | ### Communication :mailbox: 37 | 38 | At the moment these are the communication methods available, you are more than welcome to use more than one. 39 | 40 | | *Communication Method* | *Code* | 41 | | ------ | ------ | 42 | | **Mail** | `'mail'` | 43 | | **Gmail** | `'gmail'` | 44 | | **WhatsApp** | `'wapp'` | 45 | | **SMS** | `'sms'` | 46 | 47 | 48 | ### Usage :bar_chart: 49 | - Follow the instructions written inside the [example.env](example.env) and create a brand new *.env* file. 50 | 51 | - Choose a paper from Israeli Bursa to follow, the paper's ID should be inserted as a string argument of a Paper object. 52 | - Example: 53 | 54 | If we would like to follow `תכלית - MSCI World - 5124573`, we will create a new object and pass the paper's ID as a string. 55 | 56 | `paper1 = Paper('5124573')` 57 | 58 | - The program provides 2 methods to trigger an update, by daily percentage change or by a points rate change. 59 | 60 | 61 | 62 | - For a percentage change update put a percentage target. Percentage target could be either a rise or decrease in value (i.e: -0.3% or 6%). If you would like to recieve daily mail regardless of any change, just type "0". 63 | - Example: 64 | ``` 65 | paper1 = Paper('5124573') 66 | signaler.send_percentage_change(paper1, -4.7, 'wapp') 67 | ``` 68 | The code above will send us an update by WhatsApp every time the paper's daily percentage rate drops by -4.7%. 69 | 70 | - For a points rate change update put a desired rate target. 71 | For a change *above* target points rate type **`True`**, 72 | for a change *below* target points rate type **`False`**. 73 | - Example: 74 | ``` 75 | paper1 = Paper('5124573') 76 | signaler.send_rate_change(paper1, 1500, True, 'gmail') 77 | ``` 78 | The code above will send us an update by Gmail every time the paper points rate is above 1500. 79 | 80 | - Kindly note that a preffered communication method code should be typed into the 'communicate' argument regardless of the method chosen. 81 | 82 | ## Using Bizportal's API :book: 83 | 84 | Sending multiple requests during a short period of time is not respectful to the good people at Bizportal. Please abstain from abusing this code and use it as Peter Parker's Uncle would suggest. 85 | 86 | ## License 87 | [MIT](LICENSE.md) 88 | --------------------------------------------------------------------------------