The market’s been volatile recently because of the COVID-19 pandemic. I’ve been checking individual stocks manually each day for large movements. I don’t want to daytrade, but I do want to have a general feel for what the market’s doing each day. But checking manually is annoying and also not healthy, so I automated the process:

• get daily quote info from Finnhub
• set limit prices on several stocks I’m interested in
• send me an email if any limits are exceeded
• automatically repeat every day by scheduling it to run daily with launchd

Note:

I’m not interested in doing day trading, and I’m also not using this to set up automated trading. I only care about getting daily alerts if any stocks I care about exceed price limits I set for them.

Also note that this is an extremely barebones simple thing going on here. The price limits I set are hardcoded absolute numbers and not derivatives of any previous or current values. If you want to do any remotely automated or frequent trading, you should definitely use a much more robust system than what’s here.

### Getting stock info

I used Finnhub to get daily price info. It has a free API for several things. Notably, you can’t get tick data for free though. There’s also a small Python wrapper that I’m using.

I don’t want to pay for anything, and I’m not interested in using any of this for intra-daily trading, so I’m satisfied with just getting the OHLC prices per day.

Given that, it’s super simple to get the info I wanted:

from finnhub import client as Finnhub

client = Finnhub.Client(api_key="{API KEY}")

quote = client.quote(symbol='MSFT')


### Specifying stocks and limits

To specify the stocks I want to watch and what their limits should be, I put that info in a simple map:

alert_values = {
'MSFT': {
'low': 140,
'high': float('inf')
},
'DIS': {
'low': 90,
'high': 110
},
}


You can combine this and the Finnhub quote API with a simple loop to determine if the prices exceed limits.

I don’t want to have to run this script every day and parse the output.

Part of solving this is sending the info to my email account.

I did some ultra-barebones formatting around the info and put that in an email body text like MSFT is HIGH.

Then to actually send the email, I had to do a couple things. Obviously I had to write code to do so, which I’ll include below, but I also had to set up a gmail account and configure it to allow less-secure apps to connect to it.

After you create the account, you can allow less-secure apps here.

To configure the email in your code:

alerts = determine_alerts_from_finnhub()

sent_from = 'Stock Watcher'
to = ['first email to send to', 'second email to send to']

else:

subject += ' - ' + date.today().strftime('%b %d %Y')

email_text = f"""\
From: {sent_from}
To: {','.join(to)}
Subject: {subject}

{body}
"""



And then to send the email:

import smtplib

try:
server_ssl = smtplib.SMTP_SSL('smtp.gmail.com', 465)
server_ssl.ehlo()
server_ssl.sendmail(sent_from, to, email_text)
server_ssl.close()
print('email sent')
except Exception as e:
print('couldnt connect to gmail', e)


### Scheduling

Now to schedule the script so that it runs once a day, we can use launchd - assuming you’re on a Mac. I believe Linux distros have anacron instead.

The nice thing about launchd over cron is that if your computer is off when the job is scheduled to run, it’ll run the next time your computer is running.

I by no means understand launchd very well, but here’s how I set up my script to run daily:

You have to specify the settings for your job with a plist file. This is what my plist file looked like:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>whatever.label.you.want</string>
<key>KeepAlive</key>
<false/>
<true/>
<key>StandardOutPath</key>
<string>/path/to/where/you/want/to/log/stdout.log</string>
<key>StandardErrorPath</key>
<string>/path/to/where/you/want/to/log/stderr.log</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/python3</string>
<string>/path/to/your/script.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>08</integer>
<key>Minute</key>
<integer>00</integer>
</dict>
</dict>
</plist>


To touch on some values in that file:

• Label: the label for your job, basically to find it when using launchctl
• KeepAlive: will keep the script running in the background
• StandardOutPath/StandardErrorPath: any output will get appended to files you provide here
• ProgramArguments: the first arg should be the program (python3 in our case), the second arg should be your script, and any args after that would correspond to commandline-args for your script
• StartCalendarInterval: similar to cron, you can specify what interval to run a job on. Mine will run daily at 8am

Now where do you actually save that plist file? Technically, you could save it anywhere, but there are some conventional places to put it.

I put mine in /Library/LaunchAgents.

/Library/LaunchAgents $launchctl load -w your-file.plist  This will load your plist file into launchd, which means it should run at its next scheduled interval. You can see if it was properly loaded by using $ launchctl list


and seeing if it’s in the list.

If you want to test your file immediately, you can use

/Library/LaunchAgents $launchctl start whatever.label.you.chose  to run it immediately. You should be able to see output in your log files. If you messed up, and you need to edit your plist file, you can use /Library/LaunchAgents$ launchctl unload whatever.label.you.chose


If the call to your script is more complicated than just python script.py, you can stuff it into a bash -c command like so:

<key>ProgramArguments</key>
<array>
<string>bash</string>
<string>-c</string>
<string>whatever you want. e.g: cd /my/dir && source env/bin/activate && python script.py</string>
</array>


### Next steps

There’s hardly any error-checking in this script at all, so adding some checks would be smart.

For now, it’s probably okay to get by on since the launchd job writes to a logfile and I can check on that if I’m missing an email one day.

Written on March 14, 2020