Skip to content

Instantly share code, notes, and snippets.

@gurunars
Last active July 1, 2025 17:30
Show Gist options
  • Save gurunars/cb22d9bea1962e8e55228757f11de604 to your computer and use it in GitHub Desktop.
Save gurunars/cb22d9bea1962e8e55228757f11de604 to your computer and use it in GitHub Desktop.
SCHD-PE-check.ipynb
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"authorship_tag": "ABX9TyNkcmpVyotjirQG3L+SXw4F",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/gurunars/40689410c9edce3482bd786b59bcce24/schd-pe-check.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "code",
"source": [
"%pip install yahooquery"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "DvMDo9z6g-lL",
"outputId": "e9d965c4-928c-4f85-b922-b1d7ec75edb8"
},
"execution_count": 26,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Collecting yahooquery\n",
" Downloading yahooquery-2.4.1-py3-none-any.whl.metadata (4.8 kB)\n",
"Requirement already satisfied: beautifulsoup4>=4.12.2 in /usr/local/lib/python3.11/dist-packages (from yahooquery) (4.13.4)\n",
"Requirement already satisfied: curl-cffi>=0.10.0 in /usr/local/lib/python3.11/dist-packages (from yahooquery) (0.11.4)\n",
"Requirement already satisfied: lxml>=4.9.3 in /usr/local/lib/python3.11/dist-packages (from yahooquery) (5.4.0)\n",
"Requirement already satisfied: pandas>=2.2.0 in /usr/local/lib/python3.11/dist-packages (from yahooquery) (2.2.2)\n",
"Collecting requests-futures>=1.0.1 (from yahooquery)\n",
" Downloading requests_futures-1.0.2-py2.py3-none-any.whl.metadata (12 kB)\n",
"Requirement already satisfied: tqdm>=4.65.0 in /usr/local/lib/python3.11/dist-packages (from yahooquery) (4.67.1)\n",
"Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.11/dist-packages (from beautifulsoup4>=4.12.2->yahooquery) (2.7)\n",
"Requirement already satisfied: typing-extensions>=4.0.0 in /usr/local/lib/python3.11/dist-packages (from beautifulsoup4>=4.12.2->yahooquery) (4.14.0)\n",
"Requirement already satisfied: cffi>=1.12.0 in /usr/local/lib/python3.11/dist-packages (from curl-cffi>=0.10.0->yahooquery) (1.17.1)\n",
"Requirement already satisfied: certifi>=2024.2.2 in /usr/local/lib/python3.11/dist-packages (from curl-cffi>=0.10.0->yahooquery) (2025.6.15)\n",
"Requirement already satisfied: numpy>=1.23.2 in /usr/local/lib/python3.11/dist-packages (from pandas>=2.2.0->yahooquery) (2.0.2)\n",
"Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.11/dist-packages (from pandas>=2.2.0->yahooquery) (2.9.0.post0)\n",
"Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.11/dist-packages (from pandas>=2.2.0->yahooquery) (2025.2)\n",
"Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.11/dist-packages (from pandas>=2.2.0->yahooquery) (2025.2)\n",
"Requirement already satisfied: requests>=1.2.0 in /usr/local/lib/python3.11/dist-packages (from requests-futures>=1.0.1->yahooquery) (2.32.3)\n",
"Requirement already satisfied: pycparser in /usr/local/lib/python3.11/dist-packages (from cffi>=1.12.0->curl-cffi>=0.10.0->yahooquery) (2.22)\n",
"Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/dist-packages (from python-dateutil>=2.8.2->pandas>=2.2.0->yahooquery) (1.17.0)\n",
"Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests>=1.2.0->requests-futures>=1.0.1->yahooquery) (3.4.2)\n",
"Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests>=1.2.0->requests-futures>=1.0.1->yahooquery) (3.10)\n",
"Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.11/dist-packages (from requests>=1.2.0->requests-futures>=1.0.1->yahooquery) (2.4.0)\n",
"Downloading yahooquery-2.4.1-py3-none-any.whl (50 kB)\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m50.7/50.7 kB\u001b[0m \u001b[31m1.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25hDownloading requests_futures-1.0.2-py2.py3-none-any.whl (7.7 kB)\n",
"Installing collected packages: requests-futures, yahooquery\n",
"Successfully installed requests-futures-1.0.2 yahooquery-2.4.1\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"import dataclasses\n",
"from typing import List\n",
"from yahooquery import Ticker\n",
"\n",
"\n",
"@dataclasses.dataclass(frozen=True)\n",
"class EtfHolding:\n",
" ticker: str\n",
" fraction: float # 0-1, ratio of ETF holdings\n",
"\n",
"\n",
"@dataclasses.dataclass(frozen=True)\n",
"class EtfHoldingWithFundamentals:\n",
" ticker: str\n",
" pe: float\n",
" div_yield: float\n",
" fraction: float # 0-1, ratio of ETF holdings\n",
" payout_ratio: float\n",
"\n",
"\n",
"from pprint import pprint\n",
"\n",
"\n",
"def get_etf_holdings_with_fundamentals(\n",
" holdings: List[EtfHolding]\n",
") -> List[EtfHoldingWithFundamentals]:\n",
" if not holdings:\n",
" return []\n",
"\n",
" tickers = [holding.ticker for holding in holdings]\n",
" yq_tickers = Ticker(tickers)\n",
"\n",
" data = yq_tickers.get_modules(['summaryDetail'])\n",
"\n",
" #pprint(data)\n",
"\n",
" enriched_holdings: List[EtfHoldingWithFundamentals] = []\n",
"\n",
" for holding in holdings:\n",
" ticker = holding.ticker\n",
" try:\n",
" details = data.get(ticker, {})\n",
" except:\n",
" print(\"Failed to get ticker data\", ticker)\n",
"\n",
" if not isinstance(details, dict):\n",
" print(\"Details not dict\", ticker)\n",
" pprint(details)\n",
" continue\n",
"\n",
" trailing_pe = details.get('trailingPE') or details.get('forwardPE')\n",
" if trailing_pe is None:\n",
" print(\"Missing PE\", ticker)\n",
" pprint(details)\n",
" continue\n",
"\n",
" div_yeild = details.get('dividendYield') or details.get('trailingAnnualDividendYield')\n",
" if div_yeild is None:\n",
" print(\"Missing div\", ticker)\n",
" pprint(details)\n",
" continue\n",
"\n",
" payout_ratio = details.get('payoutRatio')\n",
"\n",
" if payout_ratio is None:\n",
" print(\"Missing payout\", ticker)\n",
" pprint(details)\n",
" continue\n",
"\n",
" enriched_holdings.append(\n",
" EtfHoldingWithFundamentals(\n",
" ticker=ticker,\n",
" pe=float(trailing_pe),\n",
" div_yield=float(div_yeild),\n",
" fraction=holding.fraction,\n",
" payout_ratio=float(payout_ratio)\n",
" )\n",
" )\n",
" return enriched_holdings\n"
],
"metadata": {
"id": "V4devJ8yZImY"
},
"execution_count": 123,
"outputs": []
},
{
"cell_type": "code",
"source": [
"import csv\n",
"from io import StringIO\n",
"import requests\n",
"\n",
"URL = \"https://www.schwabassetmanagement.com/sites/g/files/eyrktu361/files/product_files/SCHD/SCHD_FundHoldings_2025-07-01.CSV\"\n",
"\n",
"headers = {\n",
" \"accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n",
" \"accept-encoding\": \"gzip, deflate, br, zstd\",\n",
" \"accept-language\": \"en-US,en;q=0.9,ru;q=0.8,de;q=0.7,fi;q=0.6,fr;q=0.5\",\n",
" \"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36\",\n",
" \"sec-ch-ua\": '\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\"',\n",
" \"sec-ch-ua-mobile\": \"?0\",\n",
" \"sec-ch-ua-platform\": '\"macOS\"',\n",
" \"sec-fetch-dest\": \"document\",\n",
" \"sec-fetch-mode\": \"navigate\",\n",
" \"sec-fetch-site\": \"none\",\n",
" \"sec-fetch-user\": \"?1\",\n",
" \"upgrade-insecure-requests\": \"1\",\n",
" \"priority\": \"u=0, i\"\n",
"}\n",
"\n",
"\n",
"EXCLUDED_TICKERS = {\"GVMXX\", \"DMU5\", \"FAU5\", \"USD\"}\n",
"\n",
"TICKER_REMAP = {\n",
" \"CWENA\": \"CWEN\",\n",
"}\n",
"\n",
"\n",
"def get_holdings() -> list[EtfHolding]:\n",
" response = requests.get(URL, headers=headers, stream=True, timeout=10)\n",
" csvfile = response.text.split(\"\\n\\n\")[0]\n",
" reader = csv.DictReader(StringIO(csvfile))\n",
" holdings = [\n",
" EtfHolding(\n",
" TICKER_REMAP.get(row['Symbol'], row['Symbol']),\n",
" float(row['Percent of Assets']) / 100\n",
" )\n",
" for row in reader\n",
" ]\n",
" excluded_share = sum(\n",
" holding.fraction\n",
" for holding in holdings\n",
" if holding.ticker in EXCLUDED_TICKERS\n",
" )\n",
" residual_share = 1 - excluded_share\n",
" with_recomputed_fractions = [\n",
" EtfHolding(it.ticker, it.fraction * residual_share)\n",
" for it in holdings\n",
" if it.ticker not in EXCLUDED_TICKERS\n",
" ]\n",
" return with_recomputed_fractions"
],
"metadata": {
"id": "4NLD-VbuyAeh"
},
"execution_count": 127,
"outputs": []
},
{
"cell_type": "code",
"source": [
"holdings = get_holdings()"
],
"metadata": {
"id": "fujt5oWWAC58"
},
"execution_count": 129,
"outputs": []
},
{
"cell_type": "code",
"source": [
"fundamentals = get_etf_holdings_with_fundamentals(holdings)"
],
"metadata": {
"id": "F8TvLWh46XEE"
},
"execution_count": 130,
"outputs": []
},
{
"cell_type": "markdown",
"source": [],
"metadata": {
"id": "pk18daLqBfik"
}
},
{
"cell_type": "code",
"source": [
"weighted_pe = sum(holding.pe * holding.fraction for holding in fundamentals)\n",
"weighted_yield = sum(holding.div_yield * holding.fraction for holding in fundamentals)\n",
"weighted_payout_ratio = sum(holding.payout_ratio * holding.fraction for holding in fundamentals)\n",
"# https://www.investopedia.com/terms/p/price-earningsratio.asp\n",
"print(f\"Weighted PE: {weighted_pe}\")\n",
"print(f\"Weighted Yield: {weighted_yield}\")\n",
"# https://www.investopedia.com/terms/p/payoutratio.asp\n",
"# https://www.dividend.com/dividend-education/what-is-an-ideal-payout-ratio/\n",
"# > 80%!!!!\n",
"print(f\"Weighted Payout Ratio: {weighted_payout_ratio}\")"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "ck8uCaRC6g9z",
"outputId": "a31f1354-052f-487c-963c-94d29e26eca7"
},
"execution_count": 131,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Weighted PE: 23.830202323136024\n",
"Weighted Yield: 0.04008958669481758\n",
"Weighted Payout Ratio: 0.8874538865124144\n"
]
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment