欢迎光临
我们一直在努力

基金数据实战:从净值到持仓的全链路量化(第 8 篇):基金经理画像:任职、业绩与在管规模

基金数据实战:从净值到持仓的全链路量化(第 8 篇):基金经理画像:任职、业绩与在管规模

一、前言

前几篇我们把基金「产品」这一层的数据打通了——净值曲线、持仓明细,相当于把一只基金「长什么样」量了出来。但基金背后还有一个更关键的变量:基金经理。同一家公司、同一类策略,换一个经理,净值轨迹可能天差地别。

本篇把视角从「产品」切换到「人」,只用 jj/jjgk(基金概况,单只基金)这一个接口。注意:jjgk 是单只基金粒度,接口本身不提供任何「经理在管规模」「历任基金数」「费率性价比」等汇总指标。这些全部要靠我们用 Python 把多只基金的概况遍历后自研聚合出来。

衔接说明:本篇是「模块三·基金画像」的第一篇,从单只基金概况起步,自研经理维度聚合;下一篇(第 9 篇)将把本篇的 glr(管理人) 聚合推向「公司级」,完成「单只基金→经理→公司」的三级穿透。

二、接口字段解读

2.1 基金概况 jj/jjgk

https://api.biyingapi.com/jj/jjgk/{基金代码(如000001)}/{您的licence}

字段类型含义
dm string 基金代码
qc string 基金全称
jc string 基金简称
lx string 基金类型
pub string 发行日期
gm string 成立日期/规模
zcgm string(需转float) 资产规模
fegm string(需转float) 份额规模
glr string 基金管理人
tgr string 基金托管人
jlr string 基金经理人
ljfh string(需转float) 成立以来分红
glfv string 管理费率
tgfl string 托管费率
xsfl string 销售服务费率
rgfl string 最高认购费率
sgfl string 最高申购费率
shfl string 最高赎回费率
bjjz string 业绩比较基准
gzbd string 跟踪标的
tzmb string 投资目标
tzln string 投资理念
tzfw string 投资范围
tzcl string 投资策略
fhzc string 分红政策
fxsytz string 风险收益特征

说明:jjgk 是单只基金接口,文档暴露字段均为「该基金本身」的概况,不含有任何经理汇总或公司业绩字段。本文所有「经理/公司」指标均为自研聚合,严格基于文档暴露字段,绝不假设清单外字段(如 tenure、return 等)。其中 zcgm/fegm/ljfh 等文档标注为 string,存库前需 float() 转换并容错;tzmb 等文本描述字段直接存储即可。

三、自研衍生指标 / 业务逻辑

接口只给裸字段,以下全部用 Python 手写:

  • 经理在管总规模(total_zcgm,自研):用 jlr(基金经理人) 作分组键,遍历多只基金后把各基金的 zcgm(资产规模) 求和。jjgk 接口本身不提供该值,这是本篇聚合核心。
  • 经理历任基金数(fund_count,自研):按 jlr 分组统计基金只数(同一经理管多只基金即累加)。
  • 综合费率(comp_fee,自研):glfv(管理费率) + tgfl(托管费率) + xsfl(销售服务费率) 求和。费率越低、且 ljfh(成立以来分红) 越高,性价比越好。
  • 经理平均综合费率(avg_fee,自研):某经理旗下各基金综合费率的均值。
  • 单位规模累计分红(unit_div,自研):ljfh(成立以来分红) 数值化后与 total_zcgm 比值,衡量「每单位在管规模历史上回馈给持有人的分红强度」。
  • 公司维度预聚合(自研,为第9篇铺垫):用 glr(基金管理人) 作公司分组键,统计公司旗下基金数、合计 zcgm,为第 9 篇的公司级对比打底。
  • 缺失/异常保护:zcgm/fegm/ljfh 等转换 float 时捕获异常(空串/非数字)记 None;jlr 为 None 时跳过聚合,避免脏数据污染分组。
  • 四、数据表设计

    CREATE TABLE IF NOT EXISTS fund_profile (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT, qc TEXT, jc TEXT, lx TEXT, pub TEXT, gm TEXT,
    zcgm REAL, fegm REAL,
    glr TEXT, tgr TEXT, jlr TEXT,
    ljfh REAL,
    glfv REAL, tgfl REAL, xsfl REAL,
    rgfl REAL, sgfl REAL, shfl REAL,
    bjjz TEXT, gzbd TEXT,
    tzmb TEXT, tzln TEXT, tzfw TEXT, tzcl TEXT, fhzc TEXT, fxsytz TEXT,
    collected_at TEXT,
    UNIQUE(dm)
    );

    CREATE TABLE IF NOT EXISTS fund_manager_agg (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    jlr TEXT, — 基金经理人(分组键)
    fund_count INTEGER, — 自研:历任基金数
    total_zcgm REAL, — 自研:在管总规模
    total_ljfh REAL, — 自研:累计分红合计
    avg_fee REAL, — 自研:平均综合费率
    unit_div REAL, — 自研:单位规模累计分红
    collected_at TEXT,
    UNIQUE(jlr)
    );

    CREATE TABLE IF NOT EXISTS fund_company_agg (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    glr TEXT, — 基金管理人(公司分组键)
    fund_count INTEGER, — 自研:旗下基金数
    total_zcgm REAL, — 自研:公司合计在管规模
    collected_at TEXT,
    UNIQUE(glr)
    );

    五、完整可运行代码

    import requests
    import logging
    import time
    import pandas as pd
    import sqlite3

    # ========== 全局配置 ==========
    LICENCE = "你的licence"
    DB_PATH = "quant.db"
    LOG_FILE = "quant_collect.log"
    API_BASE = "https://api.biyingapi.com"

    # ———- 日志初始化 ———-
    logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    filemode="a"
    )
    logger = logging.getLogger(__name__)

    # ———- 交易所后缀透传(基金代码按官方原值透传) ———-
    def with_exchange_suffix(code):
    # 基金代码按官方原值透传,不做 A/B 后缀拼接,直接返回原值
    return code

    # ———- 带重试 HTTP 请求(复用) ———-
    def biying_api_get_retry(url, timeout=15, max_retry=3):
    for attempt in range(1, max_retry + 1):
    try:
    resp = requests.get(url, timeout=timeout)
    if resp.status_code == 200:
    return resp.json()
    logger.warning(f"HTTP状态码异常:{resp.status_code},第{attempt}次重试")
    except Exception as e:
    logger.warning(f"网络请求异常,第{attempt}次重试,错误信息:{str(e)}")
    time.sleep(2)
    logger.error("达到最大重试次数,接口请求失败")
    return []

    # ———- 数值安全转换(容错) ———-
    def safe_float(value):
    # zcgm/fegm/ljfh 等文档标注为 string,转换 float 时容错
    if value is None:
    return None
    s = str(value).strip()
    if s == "" or s.lower() in ("nan", "none"):
    return None
    try:
    return float(s)
    except (ValueError, TypeError):
    return None

    # ———- 单只基金概况 ———-
    def fetch_profile(code):
    code = with_exchange_suffix(code)
    url = f"{API_BASE}/jj/jjgk/{code}/{LICENCE}"
    data = biying_api_get_retry(url)
    if isinstance(data, list) and data:
    return data[0] if isinstance(data[0], dict) else {}
    if isinstance(data, dict):
    return data
    return {}

    # ———- 落库 fund_profile ———-
    def save_profile(profile):
    if not profile:
    return
    conn = sqlite3.connect(DB_PATH)
    try:
    dm = profile.get("dm")
    if dm is not None:
    conn.execute("DELETE FROM fund_profile WHERE dm = ?", (dm,))
    row = {
    "dm": profile.get("dm"),
    "qc": profile.get("qc"),
    "jc": profile.get("jc"),
    "lx": profile.get("lx"),
    "pub": profile.get("pub"),
    "gm": profile.get("gm"),
    "zcgm": safe_float(profile.get("zcgm")),
    "fegm": safe_float(profile.get("fegm")),
    "glr": profile.get("glr"),
    "tgr": profile.get("tgr"),
    "jlr": profile.get("jlr"),
    "ljfh": safe_float(profile.get("ljfh")),
    "glfv": safe_float(profile.get("glfv")),
    "tgfl": safe_float(profile.get("tgfl")),
    "xsfl": safe_float(profile.get("xsfl")),
    "rgfl": safe_float(profile.get("rgfl")),
    "sgfl": safe_float(profile.get("sgfl")),
    "shfl": safe_float(profile.get("shfl")),
    "bjjz": profile.get("bjjz"),
    "gzbd": profile.get("gzbd"),
    "tzmb": profile.get("tzmb"),
    "tzln": profile.get("tzln"),
    "tzfw": profile.get("tzfw"),
    "tzcl": profile.get("tzcl"),
    "fhzc": profile.get("fhzc"),
    "fxsytz": profile.get("fxsytz"),
    "collected_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"),
    }
    pd.DataFrame([row]).to_sql("fund_profile", conn, if_exists="append", index=False)
    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_dm ON fund_profile(dm)")
    conn.commit()
    except Exception as e:
    logger.error(f"落库基金概况失败:{str(e)}")
    finally:
    conn.close()

    # ———- 自研:经理维度聚合 ———-
    def aggregate_manager(profiles):
    """按 jlr(基金经理人) 分组,自研在管规模/历任只数/费率性价比/分红强度"""
    rows = {}
    for p in profiles:
    jlr = p.get("jlr")
    if jlr is None or str(jlr).strip() == "":
    continue # jlr 为 None 时跳过聚合
    zcgm = safe_float(p.get("zcgm"))
    ljfh = safe_float(p.get("ljfh"))
    glfv = safe_float(p.get("glfv"))
    tgfl = safe_float(p.get("tgfl"))
    xsfl = safe_float(p.get("xsfl"))
    fees = [f for f in [glfv, tgfl, xsfl] if f is not None]
    comp_fee = round(sum(fees), 4) if fees else None # 自研:综合费率
    if jlr not in rows:
    rows[jlr] = {"fund_count": 0, "total_zcgm": 0.0,
    "total_ljfh": 0.0, "fee_sum": 0.0, "fee_n": 0}
    r = rows[jlr]
    r["fund_count"] += 1
    if zcgm is not None:
    r["total_zcgm"] += zcgm
    if ljfh is not None:
    r["total_ljfh"] += ljfh
    if comp_fee is not None:
    r["fee_sum"] += comp_fee
    r["fee_n"] += 1
    result = []
    for jlr, r in rows.items():
    avg_fee = (round(r["fee_sum"] / r["fee_n"], 4)
    if r["fee_n"] > 0 else None) # 自研:平均综合费率
    unit_div = (round(r["total_ljfh"] / r["total_zcgm"], 6)
    if r["total_zcgm"] else None) # 自研:单位规模累计分红
    result.append({
    "jlr": jlr,
    "fund_count": r["fund_count"], # 自研:历任基金数
    "total_zcgm": round(r["total_zcgm"], 2), # 自研:在管总规模
    "total_ljfh": round(r["total_ljfh"], 2), # 自研:累计分红合计
    "avg_fee": avg_fee, # 自研:平均综合费率
    "unit_div": unit_div, # 自研:单位规模累计分红
    })
    return result

    # ———- 自研:公司维度预聚合 ———-
    def aggregate_company(profiles):
    """按 glr(基金管理人) 分组,为第9篇铺垫"""
    rows = {}
    for p in profiles:
    glr = p.get("glr")
    if glr is None or str(glr).strip() == "":
    continue
    zcgm = safe_float(p.get("zcgm"))
    if glr not in rows:
    rows[glr] = {"fund_count": 0, "total_zcgm": 0.0}
    r = rows[glr]
    r["fund_count"] += 1
    if zcgm is not None:
    r["total_zcgm"] += zcgm
    result = []
    for glr, r in rows.items():
    result.append({
    "glr": glr,
    "fund_count": r["fund_count"], # 自研:旗下基金数
    "total_zcgm": round(r["total_zcgm"], 2), # 自研:公司合计在管规模
    })
    return result

    # ———- 落库 fund_manager_agg(自研聚合) ———-
    def save_manager_agg(agg):
    if not agg:
    return
    conn = sqlite3.connect(DB_PATH)
    try:
    now = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
    df = pd.DataFrame(agg)
    df["collected_at"] = now
    keys = df["jlr"].tolist()
    placeholders = ",".join("?" * len(keys))
    conn.execute(
    f"DELETE FROM fund_manager_agg WHERE jlr IN ({placeholders})", keys
    )
    df.to_sql("fund_manager_agg", conn, if_exists="append", index=False)
    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_jlr ON fund_manager_agg(jlr)")
    conn.commit()
    except Exception as e:
    logger.error(f"落库经理聚合失败:{str(e)}")
    finally:
    conn.close()

    # ———- 落库 fund_company_agg(自研聚合) ———-
    def save_company_agg(agg):
    if not agg:
    return
    conn = sqlite3.connect(DB_PATH)
    try:
    now = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
    df = pd.DataFrame(agg)
    df["collected_at"] = now
    keys = df["glr"].tolist()
    placeholders = ",".join("?" * len(keys))
    conn.execute(
    f"DELETE FROM fund_company_agg WHERE glr IN ({placeholders})", keys
    )
    df.to_sql("fund_company_agg", conn, if_exists="append", index=False)
    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_glr ON fund_company_agg(glr)")
    conn.commit()
    except Exception as e:
    logger.error(f"落库公司聚合失败:{str(e)}")
    finally:
    conn.close()

    # ———- 演示入口(占位说明) ———-
    def demo(fund_list=None):
    # 以下仅为示例占位:用示例基金代码 000001 及若干占位代码,
    # 演示「遍历多只基金 + 按 jlr/glr 聚合」,不假装真实跑出全市场数字
    if fund_list is None:
    fund_list = ["000001", "000002", "000003", "000004"]
    profiles = []
    for code in fund_list:
    p = fetch_profile(code)
    if p:
    profiles.append(p)
    save_profile(p)
    mgr = aggregate_manager(profiles)
    comp = aggregate_company(profiles)
    print("\\n=== 经理维度聚合(自研) ===")
    for m in mgr:
    print(f"经理 {m['jlr']} | 在管 {m['fund_count']} 只 | "
    f"总规模 {m['total_zcgm']} | 综合费率 {m['avg_fee']} | "
    f"单位分红 {m['unit_div']}")
    print("\\n=== 公司维度聚合(自研,为第9篇铺垫) ===")
    for c in comp:
    print(f"公司 {c['glr']} | 旗下 {c['fund_count']} 只 | 总规模 {c['total_zcgm']}")
    save_manager_agg(mgr)
    save_company_agg(comp)
    print("\\n【仅为数据演示,不构成投资建议】已采集基金概况并完成经理/公司聚合")

    if __name__ == "__main__":
    demo()

    六、业务关键点

  • jjgk 是单只基金粒度:接口只给「这一只基金」的概况,任何「某经理管了多少只、总规模多大」都必须由调用方遍历多只基金后聚合,接口本身不返回汇总。
  • 字符串字段必须 float 容错:zcgm/fegm/ljfh 等文档标注为 string,且可能出现空串、非数字文本,存库前务必 safe_float 捕获异常,否则 float() 会抛错中断整批落库。
  • 分组键脏数据跳过:jlr(经理) 或 glr(公司) 为 None/空串时不能参与分组,否则会生成「空键」分组污染聚合结果,需显式 continue 跳过。
  • 自研列务必标注:total_zcgm/fund_count/avg_fee/unit_div 等都不是接口字段,是遍历聚合产物,代码注释与表设计中均已标注「自研」,避免与原始字段混淆。
  • 费率性价比为近似:综合费率用 glfv+tgfl+xsfl 求和,忽略了认购/申购/赎回费(rgfl/sgfl/shfl 多为一次性费用),真实对比应按投资周期选择口径,本文仅为演示。
  • 重跑幂等:以 dm/jlr/glr 为唯一键,落库前 DELETE 旧键再 append,保证重复执行不产生重复行、不报错。
  • 七、拓展练习

  • 把 unit_div(单位规模累计分红)从高到低排序,找出「历史分红意愿强、且综合费率低」的经理,作为费率性价比优选池。
  • 用 aggregate_company 的输出,统计全市场 glr 数量,找出「旗下基金数多、但单只平均规模小」的公司,对比「少而精」型公司,为第 9 篇公司级对比预演。
  • 八、下篇预告

    下一篇《基金公司数据:公司维度聚合与对比》将把本篇的 glr(管理人) 聚合推向「公司级」——用 jjgk 的 glr 聚合全市场基金公司(旗下基金数、总在管规模、平均费率、平均分红),再用 js/gm/kfgm(总募集规模 zmjgm、最近总份额 zjzfe) 补充规模细节,并自研「公司规模集中度」「公司平均费率」「公司分红意愿」等指标,从「单只基金→经理→公司」三级穿透收口。

    九、免责申明

    免责申明:文中所有数据处理逻辑仅为编程演示,仅为数据演示,不构成投资建议。市场有风险,投资需谨慎。

    赞(0)
    未经允许不得转载:171主机测评 » 基金数据实战:从净值到持仓的全链路量化(第 8 篇):基金经理画像:任职、业绩与在管规模
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址