Tuesday, August 11, 2026

TMX TSX TSXV

先搞清楚几个缩写

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