← 返回博客列表

【零依赖量化数据实战 #11】ETF 历史 K 线与均线:4 个 URL 做技术面回看

2026年08月25日 08:02 · 智兔数服 · 零依赖量化数据实战

摘要:【零依赖量化数据实战 #11】ETF 历史 K 线与均线:4 个 URL 做技术面回看 系列:《零依赖量化数据实战》|零依赖 · 纯 GET · 不 import 任何 SDK 适用:想做 ETF 技术面分析、K

系列:《零依赖量化数据实战》|零依赖 · 纯 GET · 不 import 任何 SDK
适用:想做 ETF 技术面分析、K 线落地、均线偏离监控,不想装库或对接多个数据源的开发者

你将得到什么

  • 4 个「ETF 历史 K 线/均线」端点的路径、返回字段和语义(历史K线 / 最新K线 / 历史MA / 最新MA),照公开文档核对过的,不是猜的
  • 一段 kline_with_ma(code) 把历史 K 线 + 历史 MA 拉齐,算出最后一日涨跌幅收盘价相对均线的偏离
  • 字段名容错写法(候选键命中),对字段名不敏感、对形态(list / dict)不敏感
  • 完整可复制运行代码,把 你的智兔token 换成真实智兔证书即可直接跑

一、四个端点,一张语义表

「技术面回看」拆成 4 个端点,路径前缀统一 /jh/hq/,且分时级别是路径参数(不是查询参数):

GET https://api.zhituapi.com/jh/hq/lskx/{code}/{level}    历史 K 线(日/周/月…)
GET https://api.zhituapi.com/jh/hq/zxkx/{code}/{level}    最新 K 线
GET https://api.zhituapi.com/jh/hq/lsma/{code}/{level}    历史 MA(移动均线,已算好)
GET https://api.zhituapi.com/jh/hq/zxma/{code}/{level}    最新 MA

鉴权统一 ?token=<你的智兔token>{code} 是基金代码,{level} 是分时级别(d=日,w=周,m=月,具体以文档示例为准)。lskx/zxkx 返回 K 线序列(每根含开高低收),lsma/zxma 返回对应的均线序列。两种形态,代码都要吃得下。

一个实际用法:先 lskx 拿历史 K 线 → 用 lsma 拿现成均线 → 算「收盘价 − 均线」的偏离,偏离为正说明价格在均线上方(偏强),为负则偏弱。这就是技术面回看。

代码形态提醒(接 #10 坑 #4):这些 {code} 用基金代码(如 510300);{level} 是路径参数,必须拼进 URL(/jh/hq/lskx/510300/d),不能写成 ?level=d

二、字段名不固定?用候选键命中

K 线/均线的返回字段名可能因数据源更新而变化。与其硬编码字段名(改了就全崩),不如用候选键列表命中抽取:

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

def _to_float(v):
    """把可能是字符串/数字/None 的值安全转 float,转不了返回 None。"""
    try:
        if v is None:
            return None
        return float(v)
    except (TypeError, ValueError):
        return None

def summarize_kline(name, data):
    """从 K 线/均线中抽取关键字段做汇总,对字段名不敏感、对形态不敏感。"""
    if data is None:
        return f"  {name}: 无数据"
    if isinstance(data, list):
        if not data:
            return f"  {name}: 0 条"
        first = data[0] if isinstance(data[0], dict) else {}
        date_key = _hit_key(first, ["date", "rq", "day", "time", "rqstr"])
        close_key = _hit_key(first, ["close", "c", "last", "dwjz", "nav", "price", "closeprice"])
        parts = [f"  {name}: {len(data)} 条"]
        if date_key:
            parts.append(f"首日={first.get(date_key)}")
        if close_key:
            parts.append(f"收盘键={first.get(close_key)}")
        return " | ".join(parts)
    if isinstance(data, dict):
        return f"  {name}: 聚合对象,键={list(data.keys())[:6]}"
    return f"  {name}: {type(data).__name__}"

这样就算数据源把 close 改成 dwjz,代码不用动。命不中也不报错——只是那行汇总少一个字段。

三、技术面回看:K 线 + 均线算偏离

核心不是「把四个接口都调通」,而是把 K 线与均线归一后算出能用的指标。我们关心两件事:

  1. 最后一日涨跌幅 =(末日收盘 − 前日收盘)/ 前日收盘
  2. 收盘价相对均线的偏离 =(末日收盘 − 均线值)/ 均线值
import sys, time
import requests

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

KLINE_ENDPOINTS = {
    "lskx": ("/jh/hq/lskx/{code}/{level}", "历史 K 线"),
    "zxkx": ("/jh/hq/zxkx/{code}/{level}", "最新 K 线"),
    "lsma": ("/jh/hq/lsma/{code}/{level}", "历史 MA"),
    "zxma": ("/jh/hq/zxma/{code}/{level}", "最新 MA"),
}


def fetch(name, code, level="d", token=TOKEN, timeout=15):
    """拉一个 K 线/均线端点,返回 (data, err)。"""
    path_tmpl, _ = KLINE_ENDPOINTS[name]
    url = f"{BASE}{path_tmpl.format(code=code, level=level)}"
    try:
        r = requests.get(url, 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 last_return_and_dev(kline, ma):
    """从 K 线算最后一日涨跌幅;从 K 线+MA 算收盘价相对均线的偏离。"""
    rows = [r for r in (kline or []) if isinstance(r, dict)]
    ck = ["close", "c", "last", "dwjz", "nav", "price", "closeprice"]
    if not rows:
        return None, None
    last = rows[-1]
    prev = rows[-2] if len(rows) >= 2 else None
    close_k = _hit_key(last, ck)
    close = _to_float(last.get(close_k)) if close_k else None
    daily_ret = None
    if prev and close_k:
        pclose = _to_float(prev.get(close_k))
        if pclose:
            daily_ret = round((close - pclose) / pclose * 100, 2)
    dev = None
    ma_rows = [r for r in (ma or []) if isinstance(r, dict)]
    if ma_rows and close is not None:
        ma_last = ma_rows[-1]
        ma_val_k = _hit_key(ma_last, ["ma", "ma5", "ma10", "ma20", "value", "avg", "mean"])
        ma_val = _to_float(ma_last.get(ma_val_k)) if ma_val_k else None
        if ma_val:
            dev = round((close - ma_val) / ma_val * 100, 2)
    return daily_ret, dev


def kline_with_ma(code, level="d", token=TOKEN, sleep=0.2):
    """拉历史 K 线 + 历史 MA,合成「收盘价 vs 均线」的技术面快照。"""
    if token == "你的智兔token":
        print("[演示] TOKEN 为占位符,下面请求会返回错误码契约;请换成真实 token 后再跑。")
    kline, kerr = fetch("lskx", code, level, token)
    ma, maerr = fetch("lsma", code, level, token)
    if kerr:
        print(f"  lskx: 暂不可用:{kerr}")
    else:
        print(summarize_kline("lskx", kline))
    if maerr:
        print(f"  lsma: 暂不可用:{maerr}")
    else:
        print(summarize_kline("lsma", ma))
    time.sleep(sleep)
    return kline, ma

sleep=0.2 是限频保护——2 个端点 + 0.2 秒间隔 ≈ 0.4 秒。要回看多只基金,把 kline_with_ma 套一层循环,外面再撑一个更大的间隔即可。

四、代码自验结果

离线自测 6 项全 PASS(真实输出,不联网):

selftest PASS: _hit_key 命中 / 列表汇总 / 聚合对象 / 空数据兜底 / 涨跌幅与偏离 / 字段名不敏感 共 6 项

其中「涨跌幅与偏离」用的是合成数据(8/19 收 3.80、8/20 收 3.90、8/21 收 4.00,均线 3.90),断言最后一日涨跌幅 = (4.00−3.90)/3.90 ≈ 2.56%、偏离 = (4.00−3.90)/3.90 ≈ 2.56%——这些是逻辑自验用的假数,不是真实行情

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

[演示] TOKEN 为占位符,下面请求会返回错误码契约;请换成真实 token 后再跑。
  lskx: 暂不可用:404 102:Licence证书(你的智兔token)不存在
  lsma: 暂不可用:404 102:Licence证书(你的智兔token)不存在

两个端点契约一致,单端点失败不影响整体流程、程序友好退出。本文未编造任何 K 线/均线数值——换成覆盖该接口的正式证书后重跑,即可打印真实技术面快照。

五、坑与注意事项

坑 #1:102 不代表路径写对了。 鉴权发生在路由匹配之前。路径故意写错配无效 token,同样返回 102。路径合法性只能靠客户端按白名单自查——本文 4 个路径是照公开文档核对的。实测 HTTP 状态码是 404 而不是 403,重点是看到 102:Licence证书... 就知道是证书问题。

坑 #2:{level} 是路径参数,不是查询参数。 必须拼成 /jh/hq/lskx/510300/d,写 ?level=d 会因缺路径参数而拿不到数据。这是本篇最容易踩的坑。

坑 #3:K 线序列顺序要确认。 不同数据源返回的 K 线可能是「时间正序」也可能是「倒序」。算「最后一日涨跌幅」前,先确认末位是最近一天(必要时 sorted 一下),否则会把最老一天当最新。

坑 #4:lskx(历史)和 zxkx(最新)别混。 lskx 是整段历史,zxkx 往往只给最近一根;要算涨跌幅用 lskx 足够,zxkx 适合做「盘中最新一根」的轻量刷新。

小结与下篇预告

这篇你拿到了 4 个 ETF 历史 K 线/均线端点(历史K线 / 最新K线 / 历史MA / 最新MA)、字段名容错抽取写法、以及一个把 K 线+均线合成「技术面快照」的模板,算出最后一日涨跌幅与均线偏离,顺带避开了 102 误判、level 路径参数、序列顺序、lskx/zxkx 混淆四个坑。

下一篇(#12)讲ETF 同类排名、规模与分红——用 /js/pm/kfpm/kfsjj_gpxjj_zsx(业绩排名)+ /js/gm/kfgm/kfsjj_gpxjj_zsx(规模)+ /js/jf/kffh/kfsjj_gpxjj_zsx(分红)做一只基金的「选基三维度」对比,把技术面升级成基本面筛选。

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

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

把代码里的 你的智兔token 换成你拿到的真实 token,上面的脚本就能直接打印 ETF 历史 K 线与均线数据。

免责声明

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

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