Stock market senior Daniel wrote a stock price real-time MacOS application in Python! Real time monitoring!

Keywords: Python Programmer security macOS

introduction

While Python is good for building many things, MacOS applications are certainly not one of them. I wonder if I can use Python to build a menu bar application for MacOS. I found it not only possible, but also "very simple".

In this tutorial, we will build a real-time MacOS application for stock prices -- all in Python.

This article requires the complete project source code to be obtained here

Let's start by installing dependencies.

rumps
requests
py2app

PyObjC

Dumps generate PyObjC applications (especially menubar applications) from simple python code. To test the dump module, run the following code:

import rumps
def hello(sender):
    print(f"Hello from {sender.title}")

app = rumps.App("Hello World")
app.menu = [rumps.MenuItem("Weird Menu Item",callback=hello)]
app.run()

This hello() function executes when a menu item is clicked.

>>> Hello from Weird Menu Item

To add more menu items, we just need to add more elements to the app.Menu list. The parameter sender indicates the MenuItem that sets the callback.

A cleaner way to get the same result is to use rumps.clicked decorator:

@rumps.clicked("Weird Menu Item")
@rumps.clicked("Saner Menu Item")
def hello(sender):
    print(f"Hello from {sender.title}")

app = rumps.App("Hello World")
app.run()

In the rest of this tutorial, we will stick to the decorator's method.

Stock price data

There are many sources to get stock quotes. We will use Finnhub's API (based on the auth key, for free).

GET https://finnhub.io/api/v1/quote?symbol=AAPL&token=YOUR_API_KEY

Sample responses:

{
   "c":119.49,
   "h":119.6717,
   "l":117.87,
   "o":119.44,
   "pc":119.21,
   "t":1605436627
}

You can go to H ttps://finnhub.io/register A free API key. At the time of writing, the rate was limited to 60 requests per minute.

Build StockApp

Start a class named "StockApp" as a subclass of the Rumps.App class, and use the rops.Click decorator to add some menu items:

import rumps
class StockApp(rumps.App):
    def __init__(self):
        super(StockApp, self).__init__(name="Stock")

    @rumps.clicked("MSFT")
    @rumps.clicked("AAPL")
    def getStock(self, sender):
        self.title = f"{sender.title}"

if __name__ == '__main__':
    StockApp().run()

It's time to integrate StockApp with the FinnHub API. Let's adjust getStock() as follows:

import requests
import rumps

class StockApp(rumps.App):
    def __init__(self):
        super(StockApp, self).__init__(name="Stock")

    @rumps.clicked("MSFT")
    @rumps.clicked("AAPL")
    def getStock(self, sender):
        response = requests.get(f"https://finnhub.io/api/v1/quote?symbol={sender.title}&token=YOUR_API_KEY")

        stock_data = response.json()['c']
        self.title = f"{sender.title}:{stock_data}"

if __name__ == '__main__':
    StockApp().run()

This getStock() method updates the title when the stock symbol is selected from the menu.

However, we don't want to get the price by clicking on the event. We need a function to constantly update the price of the selected stock, for example, every few seconds.

There is a timer class for mumps. You can use rumps.timer() to set a timer on the function, do the following.

@rumps.timer(1)
def do_something(self, sender):
   # this function is executed every 1 second

At startup, we can set some default menu items, such as "AAPL". This option can be changed by clicking the event, and the timer decoration function will continuously update the price of the currently selected menu item.

@rumps.clicked("AAPL")
@rumps.clicked("MSFT")
def changeStock(self, sender):
   self.stock = sender.title

@rumps.timer(5)
def updateStockPrice(self, sender):
   # fetch stock quote and update title

Don't complicate this, but since the application will send a network request, we need to process the API request on another thread so that the application UI can continue to run during the request processing.

import threading

@rumps.timer(5)
def updateStockPrice(self, sender):
   thread = threading.Thread(target=self.getStock)
   thread.start()

def getStock(self):
    # code to send API request 

Put everything together

Here are its views on full implementation. We added icons to make the title more attractive and added user input (using rops.window).

import threading
import requests
import rumps

class StockApp(rumps.App):
    def __init__(self):
        super(StockApp, self).__init__(name="Stock")

        self.stock = "AAPL"
        self.icon = "icon.png"
        self.API_KEY = "YOUR_API_KEY"

    @rumps.clicked("Search...")
    @rumps.clicked("MSFT")
    @rumps.clicked("TSLA")
    @rumps.clicked("NFLX")
    @rumps.clicked("FB")
    @rumps.clicked("AAPL")
    def changeStock(self, sender):
        if sender.title!="Search...":
            self.title = f" :mag: {sender.title}"
            self.stock = sender.title
        else:

            # Launches a rumps window for user input
            window = rumps.Window(f"Current: {self.stock}","Search another stock")
            window.icon = self.icon
            response = window.run()
            self.stock = response.text

    @rumps.timer(5)
    def updateStockPrice(self, sender):
        thread = threading.Thread(target=self.getStock)
        thread.start()

    def getStock(self):
        response = requests.get(f"https://finnhub.io/api/v1/quote?symbol={self.stock}&token={self.API_KEY}")

        if response.status_code!=200:
            self.title = "API Error."
            return

        stock_data = response.json()

        current_price = stock_data['c']
        previous_close = stock_data['pc']
        change = current_price-previous_close

        try:
            changePercentage = abs(round(100*(change/previous_close), 2))

            if change<0:
                marker = ":small_red_triangle_down:"
            else:
                marker = ":small_red_triangle:"

            self.title = f" {self.stock}: {str(response.json()['c'])} {marker}{changePercentage}%"

        # Finnhub returns 0 for non-existent symbols
        except ZeroDivisionError:
            self.title = "Invalid symbol, set to AAPL"
            self.stock = "AAPL"

if __name__ == '__main__':
    StockApp().run()

To run the application, you need to have an "ic.png" file in the same directory. You can download it from the link below or remove the icon from the program. Also, don't forget to assign the FinnHub API key to the self.API_KEY .

Download icon.png

Download icns

Convert it to. app

Now that the application is ready, we just need to generate a portable Mac OS application. We can use py2app to accomplish this task.

You need to have a setup.py file in the same directory. In addition, you can add an icon to the application. The file type of the MacOS application icon is. icns

StockApp
  |__ app.py
  |__ setup.py
  |__ icon.png
  |__ icon.icns

Setup.py file:

from setuptools import setup

APP = ['app.py']
DATA_FILES = ['icon.png']
OPTIONS = {
    'argv_emulation': True,
    'iconfile': 'icon.icns',
    'plist': {
        'CFBundleShortVersionString': '1.0.0',
        'LSUIElement': True,
    },
    'packages': ['rumps','requests']
}

setup(
    app=APP,
    name='Stock',
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'], install_requires=['rumps','requests']
)

Finally run setup:

python setup.py py2app

You can create a new dist folder. Open the application and see its action!

If you receive an error at runtime, please start the application through the terminal so that you can track it.

open Stock.App/Contents/MacOS/Stock

epilogue

As we can see, it's easy to create such a simple menu bar application. There are many things you can build on this, because it allows you to easily trigger Python functions. We can use a music controller, a server monitor to see if a program is running, stopwatch, CPU timer, flight position tracker, Internet speed test, to name a few.

Posted by SuisydeKing on Wed, 20 Oct 2021 19:37:51 -0700