← 返回博客列表

【零依赖量化数据实战 #04】MA/MACD/KDJ/BOLL 不用自己算:四个端点直接取指标序列

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

摘要:【零依赖量化数据实战 #04】MA/MACD/KDJ/BOLL 不用自己算:四个端点直接取指标序列 系列:《零依赖量化数据实战》|零依赖 · 纯 GET · 不 import 任何 SDK 适用:想直接拿到 A

系列:《零依赖量化数据实战》|零依赖 · 纯 GET · 不 import 任何 SDK 适用:想直接拿到 A 股均线、MACD、KDJ、布林带序列,不想自己实现指标公式的开发者

你将得到什么

  • 四个独立端点直接返回 MA / MACD / KDJ / BOLL 序列,不用装 TA-Lib、不用自己写公式
  • 每个指标的返回字段表(照源码核对过的,不是猜的)
  • 用 pandas 把四个指标合成一张宽表,并解决 KDJ 的 d 和 BOLL 的 d 列名撞车问题
  • 三个真实存在的坑:lt 不是 limit、前 N 行必然是 null、假 token 试不出参数对错
  • 完整可复制运行的代码,把 你的智兔token 换成真实证书即可直接跑

一、四个端点,一张字段表

智兔把常用指标做成了独立端点,路径规律一致:

GET https://api.zhituapi.com/hs/history/ma/{code}/{period}/{dividend}
GET https://api.zhituapi.com/hs/history/macd/{code}/{period}/{dividend}
GET https://api.zhituapi.com/hs/history/kdj/{code}/{period}/{dividend}
GET https://api.zhituapi.com/hs/history/boll/{code}/{period}/{dividend}

返回字段:

指标 返回字段 参数说明
MA t, ma3, ma5, ma10, ma15, ma20, ma30, ma60, ma120, ma200, ma250 一次给 10 条均线,不用分别请求
MACD t, diff, dea, macd, ema12, ema26 连中间量 ema12/ema26 都给了
KDJ t, k, d, j n = 9
BOLL t, u, m, d n = 20, k = 2;u=上轨 m=中轨 d=下轨

period 取值:1 5 15 30 60 d w m ydividend 取值:n f b fr br(含义见上一篇)。

注意一个容易忽略的差异:指标端点支持 1(1 分钟),而上一篇的 K 线主端点不支持。同一个 /hs/history/ 前缀下,两类端点的周期白名单不完全一样。

二、坑 #1:条数参数叫 lt,不叫 limit

上一篇 K 线端点的条数参数是 limit。指标端点同样的功能,参数名是 lt

# ✅ 指标端点
requests.get(f"{BASE}/hs/history/ma/600519/d/f", params={"token": TOKEN, "lt": 30})

# ❌ 写成 limit:不报错,但被服务端忽略,静默返回全量
requests.get(f"{BASE}/hs/history/ma/600519/d/f", params={"token": TOKEN, "limit": 30})

第二种写法不会报错。你以为拿了 30 行,实际拿到全部历史——本地内存和后续 merge 全被拖慢,而且没有任何报错提示你。这种"静默生效失败"是最难查的一类 bug。

所以把参数组装单独抽出来,用断言钉死:

def build_params(st=None, et=None, lt=None):
    """组装指标端点的查询参数。条数参数固定叫 lt——写成 limit 会被服务端忽略。"""
    params = {}
    if st:
        params["st"] = st
    if et:
        params["et"] = et
    if lt:
        params["lt"] = lt
    return params

三、坑 #2:前 N 行必然是 null,不是接口坏了

指标都有预热期。20 日均线在第 20 根 bar 才有第一个值,前 19 行只能是 null

指标 预热行数(值为 null) 原因
ma20 / BOLL 19 窗口 n=20
KDJ 8 窗口 n=9
MACD 无硬性 null 前缀 EMA 递推,从第一根就有值

这意味着两件事:

  1. 别看到 null 就以为接口有问题。 那是指标定义决定的。
  2. 想要 30 根有效的 ma20,就得多取。lt=30 只会给你 30 行,其中前 19 行 ma20 是空的,实际可用只有 11 行。要 30 行有效值,至少取 30 + 19 = 49 行。这个账要自己算。

四、坑 #3:KDJ 的 d 和 BOLL 的 d 会撞车

KDJ 返回 k, d, j,BOLL 返回 u, m, d两个 d 含义完全不同——一个是 KDJ 的 D 值(0~100),一个是布林下轨(价格)。直接 merge 会静默覆盖,你拿到一列 d 却不知道是哪个,画出来的图完全不对。

解法是取数时就加前缀隔离:

import pandas as pd

INDICATORS = {
    "ma":   ["ma5", "ma10", "ma20", "ma60"],
    "macd": ["diff", "dea", "macd"],
    "kdj":  ["k", "d", "j"],
    "boll": ["u", "m", "d"],
}

def to_frame(name, rows):
    """单指标序列 -> DataFrame,列名加前缀防重名(kdj 的 d 与 boll 的 d 会撞)。"""
    cols = INDICATORS[name]
    recs = []
    for r in rows or []:
        if not isinstance(r, dict) or not r.get("t"):
            continue                    # 丢弃无时间戳的脏行
        item = {"t": r.get("t")}
        for c in cols:
            item[f"{name}_{c}"] = r.get(c)
        recs.append(item)
    df = pd.DataFrame(recs)
    if df.empty:
        return pd.DataFrame(columns=["t"] + [f"{name}_{c}" for c in cols])
    return df

注意 df.empty 那个分支:指标取空时也要返回带列名的空表。否则下游 merge 会因为缺列直接 KeyError,一个指标失败就把整条流水线带崩。

合宽表用外连接,任一指标缺某天也不影响其他列:

def build_wide(frames):
    """按 t 外连接成宽表,时间升序。任一指标缺失也不影响其他列。"""
    wide = None
    for df in frames:
        wide = df if wide is None else wide.merge(df, on="t", how="outer")
    if wide is None or wide.empty:
        return pd.DataFrame()
    return wide.sort_values("t").reset_index(drop=True)

五、完整可运行代码

import sys, time
import pandas as pd
import requests

TOKEN = "你的智兔token"           # ← 换成你的真实智兔 token
BASE = "https://api.zhituapi.com"

INDICATORS = {
    "ma":   ["ma5", "ma10", "ma20", "ma60"],
    "macd": ["diff", "dea", "macd"],
    "kdj":  ["k", "d", "j"],
    "boll": ["u", "m", "d"],
}

def fetch_indicator(name, code, period="d", dividend="f", st=None, et=None, lt=None, token=TOKEN):
    """拉单个指标序列,返回 (rows, err)。"""
    url = f"{BASE}/hs/history/{name}/{code}/{period}/{dividend}"
    params = build_params(st, et, lt)
    params["token"] = token
    try:
        r = requests.get(url, params=params, timeout=15)
    except requests.RequestException as e:
        return None, f"网络异常:{e}"
    if r.status_code != 200:
        # 实测契约:无 token -> 400 "104:缺少token参数";无效 token -> 403 "102:Licence证书不存在"
        # 注意:鉴权发生在路由匹配之前,路径写错也会返回 102
        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]}"
    if not isinstance(payload, list):
        return None, f"非预期类型:{type(payload).__name__}"
    return [x for x in payload if isinstance(x, dict) and x.get("t")], None

def warmup_rows(name):
    """各指标的预热长度:前 N-1 行必然是 null,不是接口坏了。"""
    return {"ma": 19, "macd": 0, "kdj": 8, "boll": 19}[name]

def main():
    code, period, dividend = "600519", "d", "f"
    if TOKEN == "你的智兔token":
        print("[演示] TOKEN 为占位符,下面请求会返回错误码契约;请换成真实 token 后再跑。")

    frames, missing = [], []
    for name in ["ma", "macd", "kdj", "boll"]:
        rows, err = fetch_indicator(name, code, period, dividend, lt=30)
        if err:
            print(f"{name:<5} 暂不可用:{err}")
            missing.append(name)
            frames.append(to_frame(name, []))
        else:
            print(f"{name:<5} 返回 {len(rows)} 行(前 {warmup_rows(name)} 行按定义为 null)")
            frames.append(to_frame(name, rows))
        time.sleep(0.2)

    wide = build_wide(frames)
    if wide.empty:
        print(f"宽表为空({len(missing)} 个指标未取到数据);换成覆盖该接口的正式证书后重跑即可。")
        return
    pd.set_option("display.width", 200)
    print(wide.tail(5).to_string(index=False))

if __name__ == "__main__":
    main()

build_params / to_frame / build_wide 见前文,拼在一起就是完整脚本。)

六、代码自验结果

--selftest 不联网验 6 项,真实输出:

selftest PASS: 同名列隔离 / 外连接对齐 / 空返回列结构 / 预热长度契约 / lt 参数名 / 脏行过滤 共 6 项

其中「同名列隔离」断言 kdj_d = 55.2boll_d = 10.3 各自独立没被覆盖;「lt 参数名」断言组装结果是 {"lt": 30}limit 不在参数里。

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

[演示] TOKEN 为占位符,下面请求会返回错误码契约;请换成真实 token 后再跑。
ma    暂不可用:403 102:Licence证书(你的智兔token)不存在
macd  暂不可用:403 102:Licence证书(你的智兔token)不存在
kdj   暂不可用:403 102:Licence证书(你的智兔token)不存在
boll  暂不可用:403 102:Licence证书(你的智兔token)不存在
宽表为空(4 个指标未取到数据);换成覆盖该接口的正式证书后重跑即可。

四个端点契约一致,单指标失败不影响整体流程、程序友好退出。本文未编造任何指标数值 —— 换成覆盖该接口的正式证书后重跑,即可打印真实宽表。

顺带一个实测补充:我拿占位 token 试过非法周期 xx,四个端点全部返回 102 而不是"周期不支持"。原因和上一篇一样——鉴权在参数校验之前。所以周期合法性只能在客户端按白名单自查:

ALLOWED_PERIODS = {"1", "5", "15", "30", "60", "d", "w", "m", "y"}
assert period in ALLOWED_PERIODS, f"不支持的周期:{period}"

小结与下篇预告

这篇你拿到了 MA / MACD / KDJ / BOLL 四个指标序列,学会了合宽表防列名撞车,也避开了 lt 参数名、预热 null、假 token 试参数三个坑。

下一篇继续沪深主题,讲资金流相关端点怎么接进日常盯盘流程。

免费领取证书

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

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

把代码里的 你的智兔token 换成你拿到的真实 token,上面的脚本就能直接打印四指标宽表。

免责声明

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

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