Last active
July 1, 2025 17:30
-
-
Save gurunars/cb22d9bea1962e8e55228757f11de604 to your computer and use it in GitHub Desktop.
SCHD-PE-check.ipynb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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