Select Page

Using Investopedia’s definition of Insider Trading:

Insider trading involves trading in a public company’s stock by someone who has non-public, material information about that stock for any reason. Insider trading can be either illegal or legal depending on when the insider makes the trade.

The illegal way does not interest me, as it comes with penalties such as fines and jail time up to 20 years in prison. An example is when a CEO shares an information to a friend even before the public finds out about the information. Let’s say that the cure of a disease that their pharmaceutical company is developing will make the disease even worse. Then that friend uses that information to sell stocks and profit from the sale, before the official news comes out and the stock price plummets. The CEO and the friend could be prosecuted.

So when is insider trading legal? It can be legal as long as the transaction conforms to the rules set forth by the Securities and Exchange Commission (SEC). Those rules were passed into a law in 1934. The law is called Securities Exchange Act of 1934. When corporate insiders trade in their own company stock, they must report their trades to SEC. They file paperwork and that paperwork is all transparent in SEC website on EDGAR system, short for Electronic Data Gathering, Analysis, and Retrieval system

Directors and major shareholders have to publicly disclose it if they are buying or selling their own shares. Many investors and traders use this information to identify companies with investment potential. If insiders are using their money to buy huge amount of shares of their company, it is probably a good sign. They may have a good reason and information backing up their decision to buy.

In this blog post, I wrote a Python program that reads the Insider Trading data publicly available on the SEC website. Thanks to this post from Wrigters.io, their tutorial did most of the heavy lifting and I was able to start coding. Then I refined the logic and displayed the results on an HTML table.

Here’s a sample output of the Python code

Areas of Improvement:

  • The ticker symbols “hard-coded” in this Python code are FAANG (FB, AAPL, AMZN, NFLX and GOOG). This can be further improved by searching the filings for all 12,000+ publicly-traded companies.
  • For this sample code, “hard-coded” dates are from 2022-05-13 to 2022-06-13. Add an option to filter by filing dates gives more flexibility.
  • The output is saved in an HTML file in the local file system. It would be nice to create an API or a web application.
  • The code only displays buying and selling transactions on stocks. It can be further improved by including the options trading.

PART 1 – Setup Python project workspace

Step 1. Install Pycharm Community Edition in https://www.jetbrains.com/pycharm/download/
This tutorial uses the latest stable release pycharm-community-2021.1.2.exe

Step 2. Open Pycharm. Create New Project with a Virtual environment. The latest Pycharm installer will always have new virtual environment installed.

Step 3. Create new file with filename requirements.txt. Enter the following library names

pandas
requests>=2.25.1
yfinance
bs4
beautifulsoup4>=4.9.1
lxml

Step 4. Select “Terminal” on the bottom panel. This will launch a command line. Execute the command to install the libraries listed in requirements.txt

pip3 install -r requirements.txt

PART 2 – Writing the Python script

Write these lines of codes in main.py

Full code

import pandas as pd
import re
import copy
import requests, json
import yfinance as yf
from bs4 import BeautifulSoup
from lxml import etree
from datetime import datetime as dt
headers = {"User-Agent": "website.com email@email.com"}
TICKERS_CIK_URL = "https://www.sec.gov/files/company_tickers.json"
FILING_XML_URL = "https://www.sec.gov/Archives/edgar/data/"
FILING_SUBMISSION_URL = "https://data.sec.gov/submissions/CIK"

def make_url(cik, row):
    accession_number = row["accessionNumber"].replace("-", "")
    return f"{FILING_XML_URL}{cik}/{accession_number}/{row['accessionNumber']}.txt"


def get_element_text(doc, path):
    ret_str = ""
    if len(doc.xpath(path)) > 0:
        ret_str = doc.xpath(path)[0].text
    return ret_str


def get_transaction_type(acquired_or_disposed):
    if acquired_or_disposed == "D":
        transaction_type = "Sale"
    elif acquired_or_disposed == "A":
        transaction_type = "Purchase"
    return transaction_type


def get_transaction(elem2, total1, total2, ticker):
    transaction_date = ""
    transaction_code = ""
    valid_transaction = True
    acquired_or_disposed = ""
    transaction_form_type = ""
    total1 = 0
    total2 = 0
    for elem3 in elem2.getchildren():
        if elem3.tag == "transactiondate":
            for elem4 in elem3.getchildren():
                if elem4.tag == "value":
                    transaction_date = elem4.text
        if elem3.tag == "transactioncode":
            for elem4 in elem3.getchildren():
                if elem4.tag == "transactioncode":
                    transaction_code = elem4.text
                if elem4.tag == "transactionformtype":
                    transaction_form_type = elem4.text
            if (transaction_code != "S" and transaction_code != "P") or (transaction_form_type != "4"):
                valid_transaction = False
                break
        if elem3.tag == "transactionamounts":
            shares = 0
            price = 0
            acquired_or_disposed = ""
            for elem4 in elem3.getchildren():
                if elem4.tag == "transactionacquireddisposedcode":
                    for elem5 in elem4.getchildren():
                        if elem5.tag == "value":
                            acquired_or_disposed = elem5.text
                elif elem4.tag == "transactionshares":
                    for elem5 in elem4.getchildren():
                        if elem5.tag == "value" and elem5.text is not None:
                            shares = float(elem5.text)
                elif elem4.tag == "transactionpricepershare":
                    for elem5 in elem4.getchildren():
                        if elem5.tag == "value" and elem5.text is not None:
                            price = float(elem5.text)
                    if price == 0:
                        ticker_data = yf.Ticker(ticker)
                        ticker_data = ticker_data.history(start=transaction_date, end=transaction_date)
                        try:
                            price = ticker_data["Close"][0]
                        except Exception as e:
                            print(f"exception {e}")
                            price = 1
            if elem2.tag == "nonderivativetransaction" and acquired_or_disposed == "D":
                stock_sale_amt = price * shares
                total2 += stock_sale_amt
            elif elem2.tag == "nonderivativetransaction" and acquired_or_disposed == "A":
                stock_bought_amt = price * shares
                total1 += stock_bought_amt
    transaction_detail = {"acquired_or_disposed": acquired_or_disposed,
                          "total_stocks_bought_dollar": total1, "total_stocks_sold_dollar": total2,
                          "valid_transaction": valid_transaction}
    return transaction_detail


def create_bought_transaction(transaction):
    t = copy.copy(transaction)
    t["acquired_or_disposed"] = "A"
    t["total_stocks_sold_dollar"] = 0.0
    return t


def create_sold_transaction(transaction):
    t = copy.copy(transaction)
    t["acquired_or_disposed"] = "D"
    t["total_stocks_bought_dollar"] = 0.0
    return t


def convert_int_to_text(num):
    if num >= 1000000000:
        return str(round(float(num / 1000000000), 2)) + "B"
    elif num >= 1000000:
        return str(round(float(num / 1000000), 2)) + "M"
    elif num >= 1000:
        return str(round(float(num / 1000), 2)) + "K"
    else:
        return num


def get_document(cik, row, ticker):
    transactions = []
    url = make_url(cik, row)
    print(url)

    res = requests.get(url, headers=headers)
    res.raise_for_status()
    soup = BeautifulSoup(res.content, "html.parser")
    # use a case insensitive search for the root node of the XML document
    docs = soup.find_all(re.compile("ownershipDocument", re.IGNORECASE))
    if len(docs) > 0:
        doc = etree.fromstring(str(docs[0]))
        if docs[0].name == "ownershipdocument":
            owner = get_element_text(doc, "/ownershipdocument/reportingowner/reportingownerid/rptownername")
            is_director = get_element_text(doc, "/ownershipdocument/reportingowner/reportingownerrelationship/isdirector")
            if is_director == "1" or is_director == "true":
                is_director = "Yes"
            elif is_director == "0" or is_director == "false":
                is_director = "No"
            is_officer = get_element_text(doc, "/ownershipdocument/reportingowner/reportingownerrelationship/isofficer")
            if is_officer == "1" or is_officer == "true":
                is_officer = "Yes"
            elif is_officer == "0" or is_officer == "false":
                is_officer = "No"
            title = get_element_text(doc, "/ownershipdocument/reportingowner/reportingownerrelationship/officertitle")
            if title == "" and is_director == "Yes":
                title = "Director"
            other_text = get_element_text(doc, "/ownershipdocument/reportingowner/reportingownerrelationship/othertext")
            if title == "" and other_text is not None:
                title = other_text
            if title is None:
                title = ""
            is_ten_percent_owner = get_element_text(doc, "/ownershipdocument/reportingowner/reportingownerrelationship/istenpercentowner")
            if is_ten_percent_owner == "1" or is_ten_percent_owner == "true":
                is_ten_percent_owner = "Yes"
            elif is_ten_percent_owner == "0" or is_ten_percent_owner == "false":
                is_ten_percent_owner = "No"
            issuer_trading_symbol = get_element_text(doc, "/ownershipdocument/issuer/issuertradingsymbol")
            security = get_element_text(doc, "//securitytitle/value")
            date = get_element_text(doc, "//transactiondate/value")
            total_stocks_bought_dollar = 0
            total_stocks_sold_dollar = 0
            transaction = {}
            valid_transaction = False
            for elem in doc.getchildren():
                transaction["symbol"] = ticker
                transaction["issuer_trading_symbol"] = issuer_trading_symbol
                transaction["owner"] = owner
                transaction["security"] = security
                transaction["date"] = date
                transaction["is_director"] = is_director
                transaction["is_officer"] = is_officer
                transaction["is_ten_percent_owner"] = is_ten_percent_owner
                transaction["title"] = title
                if elem.tag == "nonderivativetable":
                    transaction = {}
                    for elem2 in elem.getchildren():
                        if elem2.tag == "nonderivativetransaction":
                            total1 = total_stocks_bought_dollar
                            total2 = total_stocks_sold_dollar
                            transaction = get_transaction(elem2, total1, total2, issuer_trading_symbol)
                            if not transaction["valid_transaction"]:
                                valid_transaction = False
                            elif transaction["valid_transaction"]:
                                if transaction["acquired_or_disposed"] == "A":
                                    total_stocks_bought_dollar += transaction["total_stocks_bought_dollar"]
                                if transaction["acquired_or_disposed"] == "D":
                                    total_stocks_sold_dollar += transaction["total_stocks_sold_dollar"]
                                valid_transaction = True
                                transaction["total_stocks_bought_dollar"] = total_stocks_bought_dollar
                                transaction["total_stocks_sold_dollar"] = total_stocks_sold_dollar
                                accession_number = row["accessionNumber"].replace("-", "")
                                transaction["url"] = f"{cik}/{accession_number}/{row['accessionNumber']}.txt"
        else:
            raise ValueError(f"Don't know how to process {docs[0].name}")

        if valid_transaction is True:
            if transaction["total_stocks_bought_dollar"] != 0:
                bought_transaction = create_bought_transaction(transaction)
                transactions.append(bought_transaction)
            if transaction["total_stocks_sold_dollar"] != 0:
                sold_transaction = create_sold_transaction(transaction)
                transactions.append(sold_transaction)
        return transactions


def main():
    r = requests.get(TICKERS_CIK_URL)
    companies = json.loads(r.content)
    tickers = ["FB", "AMZN", "AAPL", "NFLX", "GOOG"]
    cik_lookup = dict([(val["ticker"], val["cik_str"]) for key, val in companies.items()])
    rows = []
    try:
        for ticker in tickers:
            print(ticker)
            cik = cik_lookup[ticker]
            edgar_filings = requests.get(f"{FILING_SUBMISSION_URL}{cik:0>10}.json", headers=headers).json()
            recents = pd.DataFrame(edgar_filings["filings"]["recent"])
            recents["filingDate"] = pd.to_datetime(recents["filingDate"])
            insider_q1 = recents[(recents["form"] == "4") &
                                 (recents["filingDate"] >= "2022-05-13") &
                                 (recents["filingDate"] <= "2022-06-13")]
            insider_q1.shape
            for i in range(len(insider_q1)):
                transactions = get_document(cik, insider_q1.iloc[i], ticker)
                for trans in transactions:
                    rows.append(trans)
    except Exception as e:
        print(e)
        pass

    HTML_HEAD = "<html> <head> <title> </title> <link rel='stylesheet' href='/static/sorta.css'><script src='/static/sort-table.js'></script></head>"
    HTML_HEAD += "<body><table border='1' class =\'js-sort-table\'><thead><tr> <th class=\'js-sort-string\'><b>Ticker</b></th><th class=\'js-sort-string\'><b>Insider Trader</b></th> <th class=\'js-sort-string\'><b>Issuer </b></th>  " \
                 "<th class=\'js-sort-string\'><b>Director?</b></th> <th class=\'js-sort-string\'><b>Officer?</b></th>  <th class=\'js-sort-string\'><b>10% Owner</b></th> <th class=\'js-sort-string\'><b>Title</b></th>    <th class=\'js-sort-string\'><b>Transaction Date</b></th>   <th class=\'js-sort-number\'><b>Value ($)</b></th>   <th class=\'js-sort-string\'><b>Type</b></th>  <th class=\'js-sort-string\'><b>Filing Document</b></th>  </thead></tr>"
    html = HTML_HEAD
    row_ctr = 1
    for row in rows:
        row_url = f"<a href=\"{FILING_XML_URL}{row['url']}\" target=\"_blank\">Filing</a>"
        transaction_type = get_transaction_type(row['acquired_or_disposed'])
        total = 0
        if transaction_type == "Sale":
            total = round(row['total_stocks_sold_dollar'])
        elif transaction_type == "Purchase":
            total = round(row['total_stocks_bought_dollar'])
        formatted_total = "{:,}".format(total)
        html += f"<tr>    <td>{row['symbol']}</td>  <td>{row['owner']}</td>  <td>{row['issuer_trading_symbol']}</td> <td>{row['is_director']}</td> <td>{row['is_officer']}</td>   <td>{row['is_ten_percent_owner']}</td>   <td>{row['title']}</td><td>{row['date']}</td> <td align='right' title='${convert_int_to_text(total)}'>{formatted_total}</td> <td>{transaction_type}</td> <td>{row_url}</td></tr>"
        row_ctr += 1
    html += "</table><br><br></body></html>"
    now = dt.now()

    filename = "output\edgar" + now.strftime("%m%d_%H%M%S")
    file = open(f"{filename}.html", "a")
    file.write(html)


if __name__ == "__main__":
    main()

PART 3 – Running the Python Code

Step 1. From Pycharm Terminal, run

python main.py

Step 2. Open the file generated by the Python program. The filename should look like this: edgar0621_212022.html

The last column contains the link to the filing found in SEC Edgar archives. An example is this link for Filing for FB.

Congratulations!

You may also explore the official API documentation from the SEC EDGAR website https://www.sec.gov/edgar/sec-api-documentation to further improve collecting insider trading information.

References: