量化数据开发实战系列(第 25 篇):基金基础接口实战:基金列表、ETF-LOF 分类、基金概况、净值数据

量化数据开发实战系列(第 25 篇):基金基础接口实战:基金列表、ETF/LOF 分类、基金概况、净值数据

前言

第 24 篇我们完成了北交所财报、股东、股本数据的解析与存储,并预告了「基金量化专题,基金基础数据底座搭建」。做基金量化,第一步是把基金的基础数据底座搭起来:先有完整的基金清单(全市场、ETF、LOF 分类),再能按代码取到单只基金的概况、历史净值与实时行情。

本篇对接必盈基金基础相关接口,重点实现四条链路:① 全市场基金列表、ETF/LOF 分类列表;② 单只基金概况(jj/jjgk);③ 单只基金历史净值(jj/lsjz);④ 单只基金实时行情(fd/real/time)。接口只返回基金原始裸字段(基金代码、名称、净值字符串、申购/赎回状态字符串等),净值字符串转 float、日期标准化、申购赎回状态解析、净值时序入库全部靠业务代码自研。本篇把这套基础底座落库,为第 26 篇的基金持仓与业绩排行量化筛选打好地基。

一、本篇涉及接口与字段解读

以下接口地址与字段名逐字取自官方文档 ,未做任何增改;{LICENCE} 为你的授权凭证。

1.1 所有基金列表 jj/all

接口地址:https://api.biyingapi.com/jj/all/{LICENCE}

数据更新:每日 21:00;用于下方「基金概况」「历史净值」等接口的参数传入。

字段 类型 说明
dm string 基金代码,如:000001
mc string 基金名称,如:华夏成长混合
tp number 基金类型。1:开放式基金,2:封闭式基金,3:分级子基金

1.2 LOF 列表 jj/lof

接口地址:https://api.biyingapi.com/jj/lof/{LICENCE}

数据更新:每日 16:00;用于下方接口的参数传入。

字段 类型 说明
dm string 基金代码,如:sz169201
mc string 基金名称,如:浙商鼎盈 LOF

1.3 沪深基金列表 fd/list/all

接口地址:http://api.biyingapi.com/fd/list/all/{LICENCE}

数据更新:每日 16:20;获取基础的基金代码和名称,用于后续接口的参数传入。

字段 类型 说明
dm string 基金代码,如:159001.SZ
mc string 基金名称,如:货币 ETF
jys string 交易所,"sh" 表示上证,"sz" 表示深证

1.4 ETF 列表 fd/list/etf

接口地址:http://api.biyingapi.com/fd/list/etf/{LICENCE}

数据更新:每日 16:20;获取基础的 ETF 基金代码和名称。

字段 类型 说明
dm string 基金代码,如:159718.SZ
mc string 基金名称,如:港股医药 ETF
jys string 交易所,"sh" 表示上证,"sz" 表示深证

1.5 基金概况 jj/jjgk

接口地址:https://api.biyingapi.com/jj/jjgk/基金代码(如000001)/{LICENCE}

数据更新:每周六 13:00;根据《所有基金列表》得到的基金代码取该基金基本介绍。

字段 类型 说明
dm string 基金代码
qc string 基金全称
jc string 基金简称
lx string 基金类型
pub string 发行日期
gm string 成立日期/规模
zcgm string 资产规模
fegm string 份额规模
glr string 基金管理人
tgr string 基金托管人
jlr string 基金经理人
ljfh string 成立以来分红
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 风险收益特征

1.6 基金历史净值 jj/lsjz

接口地址:https://api.biyingapi.com/jj/lsjz/基金代码(如000001)/{LICENCE}

数据更新:每周六 13:00;按净值日期降序返回。

字段 类型 说明
dm string 基金代码
mc string 基金名称
t string 净值日期
dwjz string 单位净值
ljjz string 累计净值
zzl string 日增长率
sgzt string 申购状态
shzt string 赎回状态
fhsp string 分红送配

注意:dwjz/ljjz/zzl 文档类型均为 string,净值本身已是「净值」无需单位换算,但落库前必须转成 float,否则无法做数值计算与排序。

1.7 基金实时 fd/real/time

接口地址:http://api.biyingapi.com/fd/real/time/基金代码(如159001)/{LICENCE}

数据更新:盘中实时;根据《沪深基金列表》得到的基金代码获取实时交易数据(理解为日线的最新数据),券商数据源。

字段 类型 说明
p number 最新价
o number 开盘价
h number 最高价
l number 最低价
yc number 前收盘价
cje number 成交总额
v number 成交总量
pv number 原始成交总量
ud float 涨跌额
pc float 涨跌幅
zf float 振幅
t string 更新时间
pe number 市盈率
tr number 换手率
pb_ratio number 市净率
tv number 成交量

二、自研数据处理(接口只给原始裸字段,全部自研)

  1. 净值字符串转 float(自研)dwjz/ljjz/zzl 接口返回为 string,用 pd.to_numeric(errors="coerce") 统一转 float,转失败的置空丢弃,避免后续计算报错。
  2. 日期标准化(自研) :净值日期 t、实时更新时间 tpd.to_datetime(errors="coerce") 统一成 YYYY-MM-DD / 原样时间字符串。
  3. 申购/赎回状态解析(自研)sgzt/shzt 为状态字符串(如开放/暂停类文字),接口不直接给出布尔开关;自研 is_sg_open/is_sh_open 标记:字符串包含「开放」即视为可申购/可赎回(=1),否则为 0。具体枚举取值以接口实际返回为准。
  4. 基金净值时序入库(自研) :按 dm + t 联合唯一落库,支持多期净值累积成时序,供第 26 篇及后续回测使用。

接口不会返回以上任何派生值,全部基于原始裸字段用业务代码运算。

三、数据表设计

quant.db 新建 7 张表,均带 UNIQUE 防重约束:

表名 用途 唯一约束
fund_basic 所有基金列表 UNIQUE(dm)
fund_lof LOF 列表 UNIQUE(dm)
fund_hs_list 沪深基金列表 UNIQUE(dm)
fund_etf ETF 列表 UNIQUE(dm)
fund_profile 基金概况 UNIQUE(dm)
fund_nav 基金历史净值时序 UNIQUE(dm,t)
fund_realtime 基金实时行情 UNIQUE(dm,t)
sql 复制代码
CREATE TABLE IF NOT EXISTS fund_basic (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT,
    mc TEXT,
    tp INTEGER,
    UNIQUE(dm)
);

CREATE TABLE IF NOT EXISTS fund_lof (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT,
    mc TEXT,
    UNIQUE(dm)
);

CREATE TABLE IF NOT EXISTS fund_hs_list (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT,
    mc TEXT,
    jys TEXT,
    UNIQUE(dm)
);

CREATE TABLE IF NOT EXISTS fund_etf (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT,
    mc TEXT,
    jys TEXT,
    UNIQUE(dm)
);

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 TEXT, fegm TEXT, glr TEXT, tgr TEXT, jlr TEXT, ljfh TEXT,
    glfv TEXT, tgfl TEXT, xsfl TEXT, rgfl TEXT, sgfl TEXT, shfl TEXT,
    bjjz TEXT, gzbd TEXT, tzmb TEXT, tzln TEXT, tzfw TEXT, tzcl TEXT,
    fhzc TEXT, fxsytz TEXT,
    UNIQUE(dm)
);

CREATE TABLE IF NOT EXISTS fund_nav (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT, mc TEXT, t TEXT,
    dwjz REAL, ljjz REAL, zzl REAL,
    sgzt TEXT, shzt TEXT, fhsp TEXT,
    is_sg_open INTEGER, is_sh_open INTEGER,
    UNIQUE(dm,t)
);

CREATE TABLE IF NOT EXISTS fund_realtime (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dm TEXT, t TEXT,
    p REAL, o REAL, h REAL, l REAL, yc REAL,
    cje REAL, v REAL, pv REAL, ud REAL, pc REAL, zf REAL,
    pe REAL, tr REAL, pb_ratio REAL, tv REAL,
    UNIQUE(dm,t)
);

四、完整可运行代码

以下代码段开头逐字复用 本系列统一基础代码块(请求、日志、重试、股票列表),新增逻辑仅追加在函数与采集入口;演示用 LIMIT 控制规模,避免刷爆配额。

python 复制代码
import requests
import logging
import time
import pandas as pd
import numpy as np
import sqlite3
from apscheduler.schedulers.background import BackgroundScheduler

# ========== 全局配置 ==========
LICENCE = "你的licence"
DB_PATH = "quant.db"
LOG_FILE = "quant_collect.log"

# ----------日志初始化----------
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__)

# ----------带重试HTTP请求(复用)----------
def biying_api_get_retry(full_url, timeout=15, max_retry=3):
    for attempt in range(1, max_retry + 1):
        try:
            resp = requests.get(full_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 fetch_stock_list():
    url = f"http://api.biyingapi.com/hslt/list/{LICENCE}"
    return biying_api_get_retry(url)


# =====================本篇新增:基金基础数据底座=====================

# ----------通用入库(INSERT OR IGNORE 防重复)----------
def upsert_fund_table(table, df, conn):
    if df is None or df.empty:
        return
    cols = list(df.columns)
    col_str = ",".join(cols)
    placeholders = ",".join(["?"] * len(cols))
    sql = f"INSERT OR IGNORE INTO {table} ({col_str}) VALUES ({placeholders})"
    cur = conn.cursor()
    for _, row in df.iterrows():
        vals = [None if (isinstance(v, float) and pd.isna(v)) else v for v in row.tolist()]
        cur.execute(sql, vals)
    conn.commit()
    logger.info(f"{table} 入库 {len(df)} 条(含忽略重复)")


# ----------1) 所有基金列表----------
def collect_fund_all(conn, limit=50):
    url = f"https://api.biyingapi.com/jj/all/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not isinstance(raw, list) or not raw:
        logger.warning("jj/all 返回为空")
        return
    df = pd.DataFrame(raw[:limit])
    df = df.replace([None, "null", ""], np.nan)
    df["tp"] = pd.to_numeric(df["tp"], errors="coerce")
    df = df.dropna(subset=["dm"])
    upsert_fund_table("fund_basic", df, conn)


# ----------2) LOF 列表----------
def collect_fund_lof(conn, limit=50):
    url = f"https://api.biyingapi.com/jj/lof/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not isinstance(raw, list) or not raw:
        logger.warning("jj/lof 返回为空")
        return
    df = pd.DataFrame(raw[:limit])
    df = df.replace([None, "null", ""], np.nan)
    df = df.dropna(subset=["dm"])
    upsert_fund_table("fund_lof", df, conn)


# ----------3) 沪深基金列表----------
def collect_fund_hs_list(conn, limit=50):
    url = f"http://api.biyingapi.com/fd/list/all/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not isinstance(raw, list) or not raw:
        logger.warning("fd/list/all 返回为空")
        return
    df = pd.DataFrame(raw[:limit])
    df = df.replace([None, "null", ""], np.nan)
    df = df.dropna(subset=["dm"])
    upsert_fund_table("fund_hs_list", df, conn)


# ----------4) ETF 列表----------
def collect_fund_etf(conn, limit=50):
    url = f"http://api.biyingapi.com/fd/list/etf/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not isinstance(raw, list) or not raw:
        logger.warning("fd/list/etf 返回为空")
        return
    df = pd.DataFrame(raw[:limit])
    df = df.replace([None, "null", ""], np.nan)
    df = df.dropna(subset=["dm"])
    upsert_fund_table("fund_etf", df, conn)


# ----------5) 基金概况(按代码)----------
def collect_fund_profile(code, conn):
    url = f"https://api.biyingapi.com/jj/jjgk/{code}/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not raw:
        logger.warning(f"jjgk 空:{code}")
        return
    rec = raw[0] if isinstance(raw, list) else raw
    df = pd.DataFrame([rec])
    df = df.replace([None, "null", ""], np.nan)
    df = df.dropna(subset=["dm"])
    upsert_fund_table("fund_profile", df, conn)


# ----------6) 基金历史净值(按代码,自研清洗)----------
def collect_fund_nav(code, conn, limit=200):
    url = f"https://api.biyingapi.com/jj/lsjz/{code}/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not isinstance(raw, list) or not raw:
        logger.warning(f"lsjz 空:{code}")
        return
    df = pd.DataFrame(raw[:limit])
    df = df.replace([None, "null", ""], np.nan)
    # 自研:日期标准化
    df["t"] = pd.to_datetime(df["t"], errors="coerce").dt.strftime("%Y-%m-%d")
    # 自研:净值字符串转 float(净值本身已是净值,无需单位换算)
    for col in ["dwjz", "ljjz", "zzl"]:
        df[col] = pd.to_numeric(df[col], errors="coerce")
    # 自研:申购/赎回状态解析为布尔开关
    df["is_sg_open"] = df["sgzt"].astype(str).str.contains("开放", na=False).astype(int)
    df["is_sh_open"] = df["shzt"].astype(str).str.contains("开放", na=False).astype(int)
    df = df.dropna(subset=["t", "dwjz"])
    upsert_fund_table("fund_nav", df, conn)


# ----------7) 基金实时行情(按代码,自研清洗)----------
def collect_fund_realtime(code, conn):
    url = f"http://api.biyingapi.com/fd/real/time/{code}/{LICENCE}"
    raw = biying_api_get_retry(url)
    if not isinstance(raw, list) or not raw:
        logger.warning(f"real/time 空:{code}")
        return
    df = pd.DataFrame(raw)
    df = df.replace([None, "null", ""], np.nan)
    df.insert(0, "dm", code)  # 接口不返回 dm,自研回填请求代码
    # 自研:数值列统一转 float
    num_cols = ["p", "o", "h", "l", "yc", "cje", "v", "pv", "ud", "pc", "zf", "pe", "tr", "pb_ratio", "tv"]
    for col in num_cols:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors="coerce")
    df["t"] = df["t"].astype(str)
    upsert_fund_table("fund_realtime", df, conn)


# ----------建表----------
def init_fund_schema(conn):
    ddl = """
    CREATE TABLE IF NOT EXISTS fund_basic (id INTEGER PRIMARY KEY AUTOINCREMENT, dm TEXT, mc TEXT, tp INTEGER, UNIQUE(dm));
    CREATE TABLE IF NOT EXISTS fund_lof (id INTEGER PRIMARY KEY AUTOINCREMENT, dm TEXT, mc TEXT, UNIQUE(dm));
    CREATE TABLE IF NOT EXISTS fund_hs_list (id INTEGER PRIMARY KEY AUTOINCREMENT, dm TEXT, mc TEXT, jys TEXT, UNIQUE(dm));
    CREATE TABLE IF NOT EXISTS fund_etf (id INTEGER PRIMARY KEY AUTOINCREMENT, dm TEXT, mc TEXT, jys TEXT, UNIQUE(dm));
    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 TEXT, fegm TEXT, glr TEXT, tgr TEXT, jlr TEXT,
        ljfh TEXT, glfv TEXT, tgfl TEXT, xsfl TEXT, rgfl TEXT, sgfl TEXT, shfl TEXT, bjjz TEXT, gzbd TEXT,
        tzmb TEXT, tzln TEXT, tzfw TEXT, tzcl TEXT, fhzc TEXT, fxsytz TEXT, UNIQUE(dm));
    CREATE TABLE IF NOT EXISTS fund_nav (id INTEGER PRIMARY KEY AUTOINCREMENT,
        dm TEXT, mc TEXT, t TEXT, dwjz REAL, ljjz REAL, zzl REAL, sgzt TEXT, shzt TEXT, fhsp TEXT,
        is_sg_open INTEGER, is_sh_open INTEGER, UNIQUE(dm,t));
    CREATE TABLE IF NOT EXISTS fund_realtime (id INTEGER PRIMARY KEY AUTOINCREMENT,
        dm TEXT, t TEXT, p REAL, o REAL, h REAL, l REAL, yc REAL, cje REAL, v REAL, pv REAL, ud REAL, pc REAL,
        zf REAL, pe REAL, tr REAL, pb_ratio REAL, tv REAL, UNIQUE(dm,t));
    """
    conn.executescript(ddl)
    conn.commit()


# ----------演示入口(limit 控制规模)----------
def demo_run(limit=20):
    conn = sqlite3.connect(DB_PATH)
    init_fund_schema(conn)

    collect_fund_all(conn, limit=limit)        # 所有基金列表
    collect_fund_lof(conn, limit=limit)        # LOF 列表
    collect_fund_hs_list(conn, limit=limit)    # 沪深基金列表
    collect_fund_etf(conn, limit=limit)        # ETF 列表

    # 取前 limit 只基金代码,逐个拉概况/净值/实时
    sample = pd.read_sql("SELECT dm FROM fund_basic LIMIT ?", conn, params=(limit,))
    for code in sample["dm"].tolist():
        collect_fund_profile(code, conn)
        collect_fund_nav(code, conn, limit=200)
        collect_fund_realtime(code, conn)
        time.sleep(0.2)

    # 简单校验:打印已落库数量
    for tbl in ["fund_basic", "fund_lof", "fund_hs_list", "fund_etf", "fund_profile", "fund_nav", "fund_realtime"]:
        n = pd.read_sql(f"SELECT COUNT(*) AS c FROM {tbl}", conn).iloc[0]["c"]
        logger.info(f"{tbl} 共 {n} 条")

    conn.close()


if __name__ == "__main__":
    demo_run(limit=20)  # 演示仅跑前 20 只;生产可去掉 limit 跑全量

五、业务关键点

  1. 净值已是净值,勿再换算单位dwjz/ljjz 接口返回的是净值数字(string),不是「元」金额,不要做 /1e8;本篇唯一要做的是 string→float,否则排序、回测全失真。
  2. fd/real/time 不返回 dm :实时行情接口字段里没有基金代码,必须从请求参数回填 dm,否则无法关联基金。
  3. jj 系用纯 6 位代码,fd 系带交易所后缀jj/alljjgkjj/lsjz 入参是 000001 这类纯代码;fd/list/allfd/list/etffd/real/timedm 形如 159001.SZ,两者体系不同,混用会 404。
  4. 列表接口只作参数源jj/alljj/loffd/list/allfd/list/etf 的核心作用是给下游概况/净值/实时接口提供合法代码,先存列表再做 inner join 取数最稳。
  5. 入库幂等靠 UNIQUE :净值时序 UNIQUE(dm,t)、概况 UNIQUE(dm);用 INSERT OR IGNORE 而非 to_sql(append),避免重复主键报错。

六、拓展练习

  1. demo_runlimit 去掉跑全量,观察 fund_basictp=1/2/3 的数量分布,自研统计「开放式/封闭式/分级子基金」占比。
  2. fund_navdm 取最近 60 条,自研「近 60 日净值最大回撤」:(峰值净值-区间内最低净值)/峰值净值。【仅为数据演示,不构成投资建议】
  3. fund_profileglr(基金管理人)与 fund_nav 关联,自研「同一管理人旗下基金近一年净值表现」聚合视图。

七、下篇预告

系列第 26 篇(收官篇):基金重仓持仓、业绩排行接口实战,做基金量化筛选

基金基础底座(列表/概况/净值/实时)入库后,下一篇进入基金量化核心:基金重仓股、机构/社保/QFII 重仓、基金业绩排行接口实战,并按报告季度归档持仓、提取前十大重仓、做基金业绩排行统计与筛选示例------全部因子代码自研、仅为数据演示。

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

相关推荐
风中的小熊生气42 分钟前
Spring Boot 面试知识(二):分层、IoC、异常、配置与 Redis
java·springboot
张小姐的猫1 小时前
【AI大模型接入SDK】 —— 数据管理 & 与Session模块进行联动
数据结构·数据库·c++·人工智能·python·chatgpt
troy1281 小时前
Python 进阶提升(十二):自动化测试与 DevOps 实战
python·devops
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(67):MemPO——用记忆级信用分配训练 Agent 主动管理长程上下文
论文阅读·人工智能·学习·开源·github
Ai思想家1 小时前
企业AI网关的跨可用区容灾与多活设计
人工智能
Mr数据杨1 小时前
基于企业交易数据的OKVED行业分类实战解析
人工智能·数据分析·kaggle竞赛
AI推荐率1 小时前
品牌的核心能力只写在图片里,公开资料该怎样补充文字解释?
人工智能
能源革命1 小时前
DeepAR(概率自回归模型)介绍
人工智能·数据挖掘·回归
IvorySQL1 小时前
PostgreSQL 日报|大模型破解在线校验和(9 月 15 日)
数据库·人工智能·postgresql