← 返回博客列表

【零依赖量化数据实战 #09】基金持仓穿透:一只基金的股票 / 债券持仓归因

2026年08月24日 11:36 · 智兔数服 · 零依赖量化数据实战

摘要:【零依赖量化数据实战 #09】基金持仓穿透:一只基金的股票 / 债券持仓归因 | 智兔数服 发布时间 :2026 年 08 月 22 日 · 智兔数服 · 零依赖量化数据实战 摘要 :【零依赖量化数据实战 #09

发布时间:2026 年 08 月 22 日 · 智兔数服 · 零依赖量化数据实战

摘要:【零依赖量化数据实战 #09】基金持仓穿透。在 #08 拉到原始数据的基础上,用 /jh/zh/gpcc + /jh/zh/zccc + /jh/base/jjgk 三个端点做一只基金「股票 / 债券持仓归因」:前十大股票权重合计、债券持仓权重合计、第一大重仓集中度、类股型判定。全程纯 GET,不 import 任何 SDK。

系列:《零依赖量化数据实战》|零依赖 · 纯 GET · 不 import 任何 SDK
适用:做基金筛选、ETF 盯盘、持仓归因、风格判断,不想装库或对接多个数据源的开发者

你将得到什么

  • 一只基金「持仓穿透归因」的完整做法:把 #08 拉到的 gpcc(前十大重仓股)/ zccc(债券持仓)/ jjgk(概况含类型)归一成可比较的指标
  • 四个归因指标:前十大股票持仓权重合计 / 债券持仓权重合计 / 第一大重仓集中度 / 类股型判定
  • 权重归一写法:重仓字段可能是 weight / zj / proportion / ccbl,用候选键命中 + 去百分号,换数据源不用改代码
  • 一段 attribution(code) 单只基金归因的完整代码,把 你的智兔token 换成真实证书即可直接跑

一、为什么是「穿透归因」,而不是「拉出来看看」

08 的 fund_dashboard(code) 已经能把一只基金的概况、历史净值、股票持仓、债券持仓原样拉出来。但原样拉出来只是「看」,做筛选和对比还需要可比的指标

  • 两只基金都重仓茅台,谁的茅台占比更高?→ 看第一大重仓集中度
  • 一只「股票型」基金,它的权益敞口到底多大?→ 看前十大股票权重合计(注意是下界,见坑 #2)
  • 它拿了多少债券做底仓?→ 看债券持仓权重合计
  • 它是股票型还是混合型?→ 看 jjgk 概况里的类型字段

这三个端点在 #08 已经介绍过,#09 不做重复介绍,直接在它们之上做归因计算——这是同一组端点的由浅入深。

二、三个端点的语义,一张表

均见于官网公开文档、对客开放,照公开文档核对过:

GET https://api.zhituapi.com/jh/zh/gpcc/{code}    股票持仓(前十大重仓股)
GET https://api.zhituapi.com/jh/zh/zccc/{code}    债券持仓
GET https://api.zhituapi.com/jh/base/jjgk/{code}  基金概况(规模/成立日/类型)

{code} 是裸基金代码(公开文档示例 000001),不带 sz/sh 前缀(行情类才带,见坑 #3)。鉴权统一 ?token=<你的智兔token>

一个实际用法:先 gpcc 拿前十大重仓股 → 用候选键命中每只的权重字段求和,得到权益仓位下界;再 zccc 拿债券持仓求和,得到债券底仓;最后 jjgk 拿类型,判断它是不是股票型。三段拼起来就是一只基金的持仓画像。

三、字段名不固定?候选键命中 + 权重归一

重仓股返回里「权重」字段名可能是 weight / zj / proportion / ccbl,有的还带 % 号。与其硬编码,不如用候选键列表命中 + 去百分号归一:

def _to_float(v):
    """把可能是 '5.2%' / '5.2' / None 的值归一成 float。"""
    if v is None:
        return None
    if isinstance(v, (int, float)):
        return float(v)
    s = str(v).replace("%", "").replace(",", "").strip()
    try:
        return float(s)
    except ValueError:
        return None

def _hit_key(d, cands):
    """从字典中按候选键名列表命中第一个存在的键。"""
    if not isinstance(d, dict):
        return None
    for k in cands:
        if k in d:
            return k
    return None

def _w(rows, cands):
    """对一组持仓行,按候选权重键求和。"""
    total = 0.0
    for r in rows:
        k = _hit_key(r, cands)
        if k:
            v = _to_float(r.get(k))
            if v is not None:
                total += v
    return total

def _kf(r, cands):
    """取单行某候选权重键的值。"""
    k = _hit_key(r, cands)
    return _to_float(r.get(k)) if k else 0.0

这样数据源把 weight 改成 zjccbl,代码不用动;命不中只是那行少一个字段,不影响整体。

四、attribution() 一只基金的股票 / 债券持仓归因

import sys
import time
import requests

TOKEN = "你的智兔token"
BASE = "https://api.zhituapi.com"

END = {
    "jjgk": "/jh/base/jjgk/{code}",   # 基金概况(规模/成立日/类型)
    "gpcc": "/jh/zh/gpcc/{code}",     # 股票持仓(前十大重仓股)
    "zccc": "/jh/zh/zccc/{code}",     # 债券持仓
}


def fetch(path, token=TOKEN, timeout=15):
    """拉单个基金端点,返回 (data, err)。"""
    try:
        r = requests.get(f"{BASE}{path}", params={"token": token}, timeout=timeout)
    except requests.RequestException as e:
        return None, f"网络异常:{e}"
    if r.status_code != 200:
        return None, f"{r.status_code} {r.text.strip()[:140]}"
    try:
        payload = r.json()
    except ValueError:
        return None, f"非 JSON 返回:{r.text[:140]}"
    if isinstance(payload, dict):
        detail = payload.get("detail") or payload.get("error")
        return None, f"业务错误:{detail or list(payload)[:6]}"
    return payload, None


def _rows(data):
    if isinstance(data, list):
        return data
    if isinstance(data, dict):
        for k in ("data", "rows", "list", "items"):
            if isinstance(data.get(k), list):
                return data[k]
    return []


def compute_attr(code, gp_rows, zc_rows, typ):
    """把三只端点的返回归一成四个可比指标。"""
    w_cands = ["weight", "zj", "proportion", "ccbl", "ratio", "pct", "je", "mv", "sz"]
    stock_w = _w(gp_rows, w_cands)                       # 前十大股票持仓权重合计
    bond_w = _w(zc_rows, w_cands)                        # 债券持仓权重合计
    top1 = max((_kf(r, w_cands) for r in gp_rows), default=0.0)  # 第一大重仓集中度
    stock_like = ("股票" in typ) or ("股" in typ)        # 类股型判定
    return {
        "code": code,
        "type": typ,
        "stock_top10_weight": stock_w,
        "bond_weight": bond_w,
        "top1_concentration": top1,
        "stock_like": stock_like,
        "gp_rows": len(gp_rows),
        "zc_rows": len(zc_rows),
    }


def attribution(code, token=TOKEN):
    """单只基金穿透归因:股票持仓 + 债券持仓 + 概况类型。"""
    gp, e1 = fetch(END["gpcc"].format(code=code), token)
    zc, e2 = fetch(END["zccc"].format(code=code), token)
    gk, e3 = fetch(END["jjgk"].format(code=code), token)
    for e in (e1, e2, e3):
        if e:
            return None, e
    gp_rows = _rows(gp)
    zc_rows = _rows(zc)
    typ = ""
    if isinstance(gk, dict):
        typ = str(gk.get(_hit_key(gk, ["type", "type_", "fundtype", "jjlx", "category"]) or ""))
    return compute_attr(code, gp_rows, zc_rows, typ), None


def summarize_attr(a):
    if a is None:
        return "  无数据"
    parts = [
        f"  代码={a['code']}",
        f"类型={a['type'] or '未知'}",
        f"前十大股票持仓权重合计={a['stock_top10_weight']:.2f}%",
        f"债券持仓权重合计={a['bond_weight']:.2f}%",
        f"第一大重仓集中度={a['top1_concentration']:.2f}%",
        f"类股型={a['stock_like']}",
        f"股票持仓条数={a['gp_rows']} 债券持仓条数={a['zc_rows']}",
    ]
    return "\n".join(parts)


if __name__ == "__main__":
    if TOKEN == "你的智兔token":
        print("[演示] TOKEN 为占位符,请换成真实 token 后再跑。")
    for code in ("510300", "159915"):
        a, err = attribution(code)
        if err:
            print(f"  {code}: 暂不可用:{err}")
        else:
            print(f"=== {code} 持仓归因 ===")
            print(summarize_attr(a))
        time.sleep(0.2)

_to_float / _hit_key / _w / _kf 见前文,拼在一起就是完整脚本。)

五、代码自验结果

离线自测 6 项全 PASS:

selftest PASS: 端点注册完整(3/3) / _hit_key 命中逻辑 / _to_float 去百分号
/ _w 权重求和 / compute_attr 归因计算 / summarize_attr 输出 共 6 项

联网跑(占位 token)真实输出:

[演示] TOKEN 为占位符,请换成真实 token 后再跑。
  510300: 暂不可用:404 102:Licence证书(你的智兔token)不存在
  159915: 暂不可用:404 102:Licence证书(你的智兔token)不存在

3 个端点契约一致。本文未编造任何基金持仓 / 权重数值——换成覆盖该接口的正式证书后重跑,即可打印真实持仓归因。

六、坑与注意事项

坑 #1:102 不代表路径写对了。 鉴权发生在路由匹配之前。把路径故意写错配无效 token,同样返回 102。路径合法性只能靠客户端按白名单自查——本文 3 个路径是照公开文档核对的(实测返回 404 102;官网 #08 示例为 403 102,疑为接口版本差异,鉴权失败语义一致:证书不存在)。

坑 #2:gpcc 只给前十大重仓股,权重合计是股票仓位的下界。 它不含全部持仓,所以「前十大股票权重合计」只能当作权益仓位下限,不能直接当成基金的全部股票仓位——隐性持仓(第 11 名以后)没算进来。判断真实权益敞口要叠加 zccc 债券端和其他资产端一起看。

坑 #3:基金代码形态分两类。 净值 / 持仓 / 概况类(/jh/hb/lsjz/jh/base/jjgk/jh/zh/*)用裸代码(公开文档示例 000001);行情类(/jh/hq/*)的代码常带 sz/sh 前缀(示例 sz180801)。混用会拿不到数据,按公开文档示例为准。

坑 #4:类型判定用「含股票」而非精确枚举。 jjgk 的类型的字段名和取值可能随数据源变化,用 ("股票" in typ) 这种包含判定比写死 == "股票型" 更稳,漏判也只影响「类股型」这一个标签。

小结与下篇预告

这篇你在 #08 拉到原始数据的基础上,做出了一只基金的持仓归因:前十大股票权重合计、债券持仓权重合计、第一大重仓集中度、类股型判定,全部用候选键命中 + 权重归一,换数据源不用改代码。

下一批进入实时盯盘:#10 讲基金实时报价与全市场清单扫描/fund/real/ssjy + /jh/list/all + /jh/hq/etflist + /jh/hq/loflist 把全市场 ETF/LOF 报价和涨跌幅一次性拉出来)。

免费领取证书
数据来自 智兔数服(www.zhituapi.com):零 SDK、纯 GET、免费版即可起步。

领取路径:进入 www.zhituapi.com → 点击「请求证书」→「证书获取」→「免费版」(邮箱验证 3 步即可拿到 token)。

把代码里的 你的智兔token 换成你拿到的真实 token,上面的脚本就能直接打印基金持仓归因数据。

免责声明

本文仅演示公开数据接口的用法,所有代码示例均以占位 token 自验,未含任何真实基金数据;文中数据仅为接口用法演示,不构成投资建议,亦不承诺收益。投资决策请基于你自己的判断与风险承受力。

想亲自试一下?免费获取证书