先搞清楚几个缩写
TMX - TMX Group Limited
TSX- Toronto Stock Exchange,多伦多证券交易所,股票主板
TSXV - Toronto Venture Exchange,创业板
S&P - Standard & Poor's,标准普尔,简称标普
标普针对TSX市场制定了很多指数(S&P Indices),这些指数列在了这个页面的表格上。
表格第一栏是指数信息(Index Information)。
表格第二栏是指数的代码和Quote页面的链接。
表格第三栏是指数的制定方法 PDF 文件的链接和Fact Sheet链接(如果有)。
以前点击第一栏里指数信息,弹出的窗口里会列出该指数的全部成分股(Constituents),还提供了CSV文件下载,现在只列出Top 10 Constituents, 或者空白。
我的目标是取得一个指数的全部成分股数据。
以一个非常重要的指数为例:S&P/TSX 60 Index
它的quote页面是 https://money.tmx.com/en/quote/%5ETX60
它的成分股前端动态渲染页面是 https://money.tmx.com/en/quote/%5ETX60/constituents
这个页面只显示了权重最高的十大成分股,实际数据来自 TMX 自己的 GraphQL 服务 app-money.tmx.com/graphql;这个 GraphQL endpoint 本身目前仍在线。现在来直接抓 GraphQL。
我用 Windows 11,通过 Firefox 的 Web Developer Tools 找出 TMX 自己的请求。
打开 https://money.tmx.com/en/quote/%5ESPTXBMCP/constituents
然后 F12 → Network
选择 XHR
Ctrl + F5 强制刷新页面
搜索 graphql
应该会看到发往 https://app-money.tmx.com/graphql 的 POST 请求。
点数据请求量最大的那一行,再看 Request,找到类似:
{
"operationName": "xxxxx",
"variables": {
...
},
"query": "query xxxxx(...) {...}"
}
我截图如下:
点 Response 可以看到完整的 GraphQL 返回结果。有了 Request 里的数据,现在已经可以完全脱离浏览器,直接用 Python 调 TMX 的 GraphQL API,自动取得 ^SPTXBMCP 的全部成分股。
抓到的核心接口是:
POST https://app-money.tmx.com/graphql
operationName: getIndexConstituents
variables: {"symbol":"^TX60"}
用下面这个代码就可以抓取全部的成分股,还会自动验证成分股数量和权重合计:
import requests
import pandas as pd
URL = "https://app-money.tmx.com/graphql"
payload = {
"operationName": "getIndexConstituents",
"variables": {
"symbol": "^TX60"
},
"query": """
query getIndexConstituents($symbol: String!) {
constituents: getIndexConstituents(symbol: $symbol) {
symbol
quotedMarketValue
longName
shortName
weight
exShortName
exchange
exLongName
__typename
}
keyData: getIndexKeyData(symbol: $symbol) {
adjMarketCap
avgConstituentMarketCap
numConstituents
top10HoldingsAdjMarketCap
ytdPriceReturn
prevDayPriceReturn
prevMonthPriceReturn
prevQuarterPriceReturn
percentWeightLargestConstituent
peRatio
pbRatio
priceToSales
divYield
pcfRatio
__typename
}
}
"""
}
headers = {
"Accept": "*/*",
"Content-Type": "application/json",
"locale": "en",
"Origin": "https://money.tmx.com",
"Referer": "https://money.tmx.com/",
"User-Agent": "Mozilla/5.0"
}
response = requests.post(
URL,
json=payload,
headers=headers,
timeout=30
)
response.raise_for_status()
result = response.json()
# GraphQL 本身即使 HTTP 200,也可能返回 errors
if "errors" in result:
raise RuntimeError(result["errors"])
constituents = result["data"]["constituents"]
key_data = result["data"]["keyData"]
df = pd.DataFrame(constituents)
# 删除 GraphQL 内部字段
df = df.drop(columns=["__typename"], errors="ignore")
# 调整列顺序
df = df[
[
"symbol",
"longName",
"exchange",
"exShortName",
"quotedMarketValue",
"weight",
"shortName",
"exLongName"
]
]
print(df.to_string(index=False))
print("\n------------------------------")
print("TMX reported constituents :", key_data["numConstituents"])
print("Actually downloaded :", len(df))
print("Weight total :", df["weight"].sum())
print("------------------------------")
# 完整数据
df.to_csv(
"SPTXBMCP_constituents.csv",
index=False,
encoding="utf-8-sig"
)
df.to_excel(
"SPTXBMCP_constituents.xlsx",
index=False
)
print("\nSaved:")
print("TX60_constituents.csv")
print("TX60_constituents.xlsx")

No comments:
Post a Comment