量化数据开发实战系列(第 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 | 成交量 |
二、自研数据处理(接口只给原始裸字段,全部自研)
- 净值字符串转 float(自研) :
dwjz/ljjz/zzl接口返回为 string,用pd.to_numeric(errors="coerce")统一转 float,转失败的置空丢弃,避免后续计算报错。 - 日期标准化(自研) :净值日期
t、实时更新时间t用pd.to_datetime(errors="coerce")统一成YYYY-MM-DD/ 原样时间字符串。 - 申购/赎回状态解析(自研) :
sgzt/shzt为状态字符串(如开放/暂停类文字),接口不直接给出布尔开关;自研is_sg_open/is_sh_open标记:字符串包含「开放」即视为可申购/可赎回(=1),否则为 0。具体枚举取值以接口实际返回为准。 - 基金净值时序入库(自研) :按
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 跑全量
五、业务关键点
- 净值已是净值,勿再换算单位 :
dwjz/ljjz接口返回的是净值数字(string),不是「元」金额,不要做/1e8;本篇唯一要做的是 string→float,否则排序、回测全失真。 fd/real/time不返回dm:实时行情接口字段里没有基金代码,必须从请求参数回填dm,否则无法关联基金。jj系用纯 6 位代码,fd系带交易所后缀 :jj/all、jjgk、jj/lsjz入参是000001这类纯代码;fd/list/all、fd/list/etf、fd/real/time的dm形如159001.SZ,两者体系不同,混用会 404。- 列表接口只作参数源 :
jj/all、jj/lof、fd/list/all、fd/list/etf的核心作用是给下游概况/净值/实时接口提供合法代码,先存列表再做 inner join 取数最稳。 - 入库幂等靠 UNIQUE :净值时序
UNIQUE(dm,t)、概况UNIQUE(dm);用INSERT OR IGNORE而非to_sql(append),避免重复主键报错。
六、拓展练习
- 把
demo_run的limit去掉跑全量,观察fund_basic中tp=1/2/3的数量分布,自研统计「开放式/封闭式/分级子基金」占比。 - 用
fund_nav按dm取最近 60 条,自研「近 60 日净值最大回撤」:(峰值净值-区间内最低净值)/峰值净值。【仅为数据演示,不构成投资建议】 - 将
fund_profile的glr(基金管理人)与fund_nav关联,自研「同一管理人旗下基金近一年净值表现」聚合视图。
七、下篇预告
系列第 26 篇(收官篇):基金重仓持仓、业绩排行接口实战,做基金量化筛选
基金基础底座(列表/概况/净值/实时)入库后,下一篇进入基金量化核心:基金重仓股、机构/社保/QFII 重仓、基金业绩排行接口实战,并按报告季度归档持仓、提取前十大重仓、做基金业绩排行统计与筛选示例------全部因子代码自研、仅为数据演示。
免责申明:文中所有数据处理逻辑仅为编程演示,仅为数据演示,不构成投资建议。市场有风险,投资需谨慎。