欢迎光临
我们一直在努力

量化数据进阶:多维实战篇(第 8 篇):MACD:金叉死叉与柱状动能

量化数据进阶:多维实战篇(第 8 篇):MACD:金叉死叉与柱状动能

一、前言

上一篇《KDJ与BOLL:超买超卖与布林带宽》我们用 hsstock/history/kdj 与 hsstock/history/boll 补齐了「位置」这一维度——KDJ 回答「是不是超买超卖」,BOLL 回答「波动是不是压到极限」。但位置和方向的确认,都还缺少一个关键维度:动能。股价可以在低位横盘很久(KDJ 超卖、带宽收敛),却迟迟不启动,缺的就是动能的积累与释放。

本篇收束到本模块技术指标的最后一块拼图——趋势动能,使用接口:

  • hsstock/history/macd:官方 MACD,一次返回时间 t 与 diff、dea、macd、ema12、ema26 五个字段。

一个字段串起三层信息:diff 与 dea 的上下穿构成金叉死叉(方向),macd 柱的正负与缩放构成柱状动能(强度),ema12 与 ema26 的差值提供快慢均线的交叉验证(确认)。三者叠起来,就形成「方向 + 强度 + 确认」的完整动能刻画。

需要特别提醒:该接口只返回指标值本身,不返回原始收盘价。因此教科书里「股价创新高、MACD 不创新高」的顶背离无法直接算——若要严格做价格与指标的背离,收盘价必须从其他接口(如历史 K 线类接口)另行获取后在本地按时间对齐。本文明确改用指标序列自身来实现:diff 创新高但 macd 柱绝对值较前高收缩,标记为「动能衰竭」。全文会反复强调这是指标层面的近似,不是严格意义上的价格背离,且不做超出文档的字段假设。

衔接说明:本篇是「模块三·历史技术指标」的第三篇,上一篇解决了「超买超卖与波动区间」,本篇解决「趋势动能」,也是本模块技术指标的最后一块拼图。下一篇将进入「自研指标」战场,彻底摆脱对官方指标接口的依赖。

二、接口字段解读

2.1 官方 MACD hsstock/history/macd

http://api.biyingapi.com/hsstock/history/macd/{代码.市场}/{级别}/{除权方式}/{LICENCE}?st={开始日期}&et={结束日期}&lt={返回条数}

路径参数:

参数取值说明
代码.市场 如 600519.SH、000001.SZ 股票代码 + 交易所后缀
级别 5 / 15 / 30 / 60 / d / w / m / y 分钟级 / 日 / 周 / 月 / 年
除权方式 n / f / b / fr / br 不复权 / 前复权 / 后复权 / 前复权(含分红) / 后复权(含分红),分钟级仅支持 n

查询参数:

参数说明
st 开始日期,如 20240101
et 结束日期,如 20241231
lt 返回条数

返回字段(仅以下字段):

字段类型含义
t string 时间(该根 K 线对应的交易时间)
diff number DIF 线(快线),即 12 日 EMA 与 26 日 EMA 之差
dea number DEA 线(慢线 / 信号线),即 DIF 的 9 日 EMA
macd number MACD 柱,通常为 2 × (diff − dea),正负对应红绿柱
ema12 number 12 日指数均线(MACD 的中间计算量)
ema26 number 26 日指数均线(MACD 的中间计算量)

说明:接口只给五个裸指标值,既不给原始收盘价,也不给 9 日 EMA 的周期参数。因此金叉死叉、零轴分区、柱状动能、动能衰竭、双线粘合度等,全部由这五个值手写推导。

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

接口只给裸指标值,以下全部用 Python 手写:

  • DIF/DEA 金叉死叉(自研):用前后两根 bar 的位置关系判定——上一根 diff <= dea 且当前 diff > dea 为金叉(返回 1);上一根 diff >= dea 且当前 diff < dea 为死叉(返回 -1);否则返回 0。首根 bar 没有前值,返回 None 而非 0。
  • 零轴分区(自研):diff > 0 且 dea > 0 标记「零轴上方多头区」;diff < 0 且 dea < 0 标记「零轴下方空头区」;其余(一正一负、或任一为 0)标记「穿越区」。分区只看两条线相对零轴的正负,与金叉死叉是两套独立信号。
  • 柱状动能(自研):取 macd 字段本身作为柱值。macd > 0 记红柱、macd < 0 记绿柱、macd == 0 记无柱;再统计「同号连续根数」——当前柱与前一根同号则累加,符号翻转即归零重新计数。
  • 柱体缩放(自研):比较相邻两根柱体绝对值 |macd|,当期大于上期标记「放大」,小于标记「缩小」,相等(或在阈值内)标记「持平」。绝对值参与比较,是因为红柱与绿柱的「力量」只与长度有关。
  • 柱体连续放大计数(自研):从当前根往回数,连续多少根满足 |macd| 严格递增,计数即为「动能持续」根数。计数 >= 2 通常意味着动能正在被持续释放,计数为 0 说明当根已出现收缩。
  • 动能衰竭标记(自研,指标层面近似):在回看窗口(默认 20 根)内,若当前 diff 创出窗口新高(且为正),但当前 |macd| 小于窗口内 |macd| 的最大值,则标记「动能衰竭」(返回 1),否则返回 0;窗口内有效样本不足时返回 None。再次强调:严格背离需要收盘价参与,而该接口不返回收盘价,本文用 diff 自身新高对比柱值收缩来实现,是指标层面的近似,不是价格背离。
  • 双线粘合度(自研):|diff – dea| / |diff|,取值非负,越小说明快慢线越贴近、越接近变盘;diff 为 0 时分母为零,返回 None。注意该值无量纲,可跨价位横向比较。
  • ema12 / ema26 辅助用法(自研):两者差值 ema12 – ema26 的符号,可作为快慢均线关系的交叉验证——与 diff 同号视为「快慢线共振」(diff > 0 且差值 > 0 记「多头共振」,diff < 0 且差值 < 0 记「空头共振」),异号则记「快慢线分歧」。
  • null 值防御:以上 8 项在计算前统一用 pd.isna 判空,任一参与值为空即返回 None(或跳过),绝不让 None 参与比较与四则运算。
  • 四、数据表设计

    CREATE TABLE IF NOT EXISTS stock_macd (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT, level TEXT, t TEXT,
    diff REAL, dea REAL, macd REAL,
    ema12 REAL, ema26 REAL,
    collected_at TEXT,
    UNIQUE(code, level, t)
    );

    — 可选:自研信号表
    CREATE TABLE IF NOT EXISTS macd_signal (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT, level TEXT, t TEXT,
    diff REAL, dea REAL, macd REAL, ema12 REAL, ema26 REAL,
    cross INTEGER, — 自研:1 金叉 / -1 死叉 / 0 无信号 / NULL 首根
    zone TEXT, — 自研:零轴上方多头区 / 零轴下方空头区 / 穿越区
    bar_sign INTEGER, — 自研:1 红柱 / -1 绿柱 / 0 无柱
    bar_streak INTEGER, — 自研:同号柱连续根数
    bar_trend TEXT, — 自研:放大 / 缩小 / 持平
    momentum_streak INTEGER, — 自研:|macd| 连续递增根数(动能持续)
    exhaust INTEGER, — 自研:1 动能衰竭 / 0 否 / NULL 样本不足
    stickiness REAL, — 自研:双线粘合度 |diff-dea|/|diff|
    ema_gap REAL, — 自研:ema12 – ema26
    ema_check TEXT, — 自研:多头共振 / 空头共振 / 快慢线分歧
    UNIQUE(code, level, t)
    );

    说明:macd_signal 中除 code / level / t / diff / dea / macd / ema12 / ema26 外,其余列全部是本文自研衍生列,不是接口字段。

    五、完整可运行代码

    import requests
    import logging
    import time
    import sqlite3
    from datetime import datetime
    import pandas as pd
    import numpy as np

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

    # 接口返回的全部字段(严格对照官方文档,不增不减)
    MACD_FIELDS = ["t", "diff", "dea", "macd", "ema12", "ema26"]

    # 落库列(code / level 为本地维度字段,collected_at 为本地采集时间)
    MACD_COLUMNS = ["code", "level", "t", "diff", "dea", "macd",
    "ema12", "ema26", "collected_at"]

    # 自研信号列
    SIGNAL_COLUMNS = [
    "code", "level", "t",
    "diff", "dea", "macd", "ema12", "ema26",
    "cross", "zone", "bar_sign", "bar_streak", "bar_trend",
    "momentum_streak", "exhaust", "stickiness", "ema_gap", "ema_check",
    ]

    MINUTE_LEVELS = ("5", "15", "30", "60")

    # ———- 日志初始化 ———-
    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 with_exchange_suffix(code):
    code = str(code).strip()
    if code.startswith("60") or code.startswith("68") or code.startswith("9"):
    return code + ".SH"
    if code.startswith("00") or code.startswith("30") or code.startswith("2"):
    return code + ".SZ"
    if code.startswith("8") or code.startswith("4"):
    return code + ".BJ"
    return code + ".SH"

    # ———- 拉取官方 MACD ———-
    def fetch_history_macd(code, level="d", adjust="n", start="", end="", limit=250):
    if str(level) in MINUTE_LEVELS:
    adjust = "n"
    dm = with_exchange_suffix(code)
    url = (f"{API_BASE}/hsstock/history/macd/{dm}/{level}/{adjust}/{LICENCE}"
    f"?st={start}&et={end}&lt={limit}")
    data = biying_api_get_retry(url)
    if isinstance(data, dict):
    data = data.get("data", [])
    if not isinstance(data, list):
    return []
    return [row for row in data if isinstance(row, dict)]

    # ———- pandas 清洗链 ———-
    def clean_macd_frame(rows):
    df = pd.DataFrame(rows)
    if df.empty:
    return df
    for col in MACD_FIELDS:
    if col not in df.columns:
    df[col] = np.nan
    df = df.reindex(columns=MACD_FIELDS)
    df["t"] = pd.to_datetime(df["t"], errors="coerce")
    for col in ("diff", "dea", "macd", "ema12", "ema26"):
    df[col] = pd.to_numeric(df[col], errors="coerce")
    df = df.dropna(subset=["t"])
    df = df.sort_values("t")
    df = df.drop_duplicates(subset=["t"], keep="last")
    df = df.reset_index(drop=True)
    return df

    # ———- 自研指标 1:DIF/DEA 金叉死叉 ———-
    def detect_macd_cross(prev_diff, prev_dea, curr_diff, curr_dea):
    """返回值:1 金叉 / -1 死叉 / 0 无信号 / None 首根无前值或数据缺失"""
    if pd.isna(prev_diff) or pd.isna(prev_dea) or pd.isna(curr_diff) or pd.isna(curr_dea):
    return None
    if prev_diff <= prev_dea and curr_diff > curr_dea:
    return 1
    if prev_diff >= prev_dea and curr_diff < curr_dea:
    return –1
    return 0

    # ———- 自研指标 2:零轴分区 ———-
    def judge_zero_zone(diff, dea):
    if pd.isna(diff) or pd.isna(dea):
    return None
    if diff > 0 and dea > 0:
    return "零轴上方多头区"
    if diff < 0 and dea < 0:
    return "零轴下方空头区"
    return "穿越区"

    # ———- 自研指标 3:柱体符号 ———-
    def judge_bar_sign(bar):
    if pd.isna(bar):
    return None
    if bar > 0:
    return 1
    if bar < 0:
    return –1
    return 0

    # ———- 自研指标 3:同号柱连续根数 ———-
    def calc_bar_streak(signs, idx):
    """从当前根往回数,统计同号柱连续出现多少根"""
    if idx < 0 or idx >= len(signs):
    return None
    curr = signs[idx]
    if curr is None or pd.isna(curr) or curr == 0:
    return 0
    streak = 0
    i = idx
    while i >= 0:
    prev = signs[i]
    if prev is None or pd.isna(prev) or prev != curr:
    break
    streak += 1
    i -= 1
    return streak

    # ———- 自研指标 4:柱体缩放 ———-
    def judge_bar_trend(prev_bar, curr_bar, eps=0.0):
    """比较相邻两根柱体绝对值:放大 / 缩小 / 持平"""
    if prev_bar is None or curr_bar is None:
    return None
    if pd.isna(prev_bar) or pd.isna(curr_bar):
    return None
    delta = curr_bar – prev_bar
    if delta > eps:
    return "放大"
    if delta < –eps:
    return "缩小"
    return "持平"

    # ———- 自研指标 5:柱体连续放大计数 ———-
    def calc_momentum_streak(bars, idx):
    """从当前根往回数,|macd| 严格递增的连续根数(动能持续)"""
    if idx < 0 or idx >= len(bars):
    return None
    curr = bars[idx]
    if curr is None or pd.isna(curr):
    return None
    streak = 0
    i = idx
    while i > 0:
    prev = bars[i – 1]
    now = bars[i]
    if prev is None or pd.isna(prev) or now is None or pd.isna(now):
    break
    if now > prev:
    streak += 1
    i -= 1
    else:
    break
    return streak

    # ———- 自研指标 6:动能衰竭(指标层面近似) ———-
    def judge_exhaust(diffs, bars, idx, window=20):
    """1 动能衰竭 / 0 否 / None 样本不足
    判定:窗口内 diff 创新高,但当前 |macd| 小于窗口内 |macd| 的最大值。
    注意:接口不返回收盘价,这里是 diff 自身新高与柱值收缩的对比,非价格背离。
    """

    if idx <= 0 or idx >= len(diffs):
    return None
    start = max(0, idx – window)
    hist_diffs = [v for v in diffs[start:idx] if v is not None and not pd.isna(v)]
    hist_bars = [v for v in bars[start:idx] if v is not None and not pd.isna(v)]
    curr_diff = diffs[idx]
    curr_bar = bars[idx]
    if not hist_diffs or not hist_bars:
    return None
    if curr_diff is None or pd.isna(curr_diff) or curr_bar is None or pd.isna(curr_bar):
    return None
    if curr_diff <= 0 or max(hist_diffs) <= 0:
    return 0
    if curr_diff < max(hist_diffs):
    return 0
    return 1 if curr_bar < max(hist_bars) else 0

    # ———- 自研指标 7:双线粘合度 ———-
    def calc_stickiness(diff, dea):
    """|diff – dea| / |diff|,越小越接近变盘;diff 为 0 返回 None"""
    if pd.isna(diff) or pd.isna(dea) or diff == 0:
    return None
    return round(abs(diff – dea) / abs(diff), 4)

    # ———- 自研指标 8:ema12 / ema26 辅助验证 ———-
    def calc_ema_gap(ema12, ema26):
    if pd.isna(ema12) or pd.isna(ema26):
    return None
    return round(ema12 – ema26, 4)

    def judge_ema_check(diff, ema_gap):
    if pd.isna(diff) or ema_gap is None or pd.isna(ema_gap):
    return None
    if diff > 0 and ema_gap > 0:
    return "多头共振"
    if diff < 0 and ema_gap < 0:
    return "空头共振"
    return "快慢线分歧"

    # ———- 逐根 bar 生成信号表 ———-
    def build_signal_frame(df, exhaust_window=20):
    if df.empty:
    return pd.DataFrame(columns=SIGNAL_COLUMNS)
    diffs = [pd.to_numeric(v, errors="coerce") for v in df["diff"].tolist()]
    deas = [pd.to_numeric(v, errors="coerce") for v in df["dea"].tolist()]
    macds = [pd.to_numeric(v, errors="coerce") for v in df["macd"].tolist()]
    bars = [abs(v) if not pd.isna(v) else np.nan for v in macds]
    signs = [judge_bar_sign(v) for v in macds]

    records = []
    for idx, (_, row) in enumerate(df.iterrows()):
    ema_gap = calc_ema_gap(
    pd.to_numeric(row.get("ema12"), errors="coerce"),
    pd.to_numeric(row.get("ema26"), errors="coerce"),
    )
    prev_diff = diffs[idx – 1] if idx > 0 else np.nan
    prev_dea = deas[idx – 1] if idx > 0 else np.nan
    prev_bar = bars[idx – 1] if idx > 0 else None
    records.append({
    "t": row["t"],
    "diff": diffs[idx],
    "dea": deas[idx],
    "macd": macds[idx],
    "ema12": pd.to_numeric(row.get("ema12"), errors="coerce"),
    "ema26": pd.to_numeric(row.get("ema26"), errors="coerce"),
    "cross": detect_macd_cross(prev_diff, prev_dea, diffs[idx], deas[idx]),
    "zone": judge_zero_zone(diffs[idx], deas[idx]),
    "bar_sign": signs[idx],
    "bar_streak": calc_bar_streak(signs, idx),
    "bar_trend": judge_bar_trend(prev_bar, bars[idx]),
    "momentum_streak": calc_momentum_streak(bars, idx),
    "exhaust": judge_exhaust(diffs, bars, idx, window=exhaust_window),
    "stickiness": calc_stickiness(diffs[idx], deas[idx]),
    "ema_gap": ema_gap,
    "ema_check": judge_ema_check(diffs[idx], ema_gap),
    })
    return pd.DataFrame(records)

    # ———- 建表 ———-
    def init_db():
    conn = sqlite3.connect(DB_PATH)
    try:
    conn.execute("""
    CREATE TABLE IF NOT EXISTS stock_macd (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT, level TEXT, t TEXT,
    diff REAL, dea REAL, macd REAL,
    ema12 REAL, ema26 REAL,
    collected_at TEXT,
    UNIQUE(code, level, t)
    )
    """
    )
    conn.execute("""
    CREATE TABLE IF NOT EXISTS macd_signal (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT, level TEXT, t TEXT,
    diff REAL, dea REAL, macd REAL, ema12 REAL, ema26 REAL,
    cross INTEGER, zone TEXT, bar_sign INTEGER,
    bar_streak INTEGER, bar_trend TEXT,
    momentum_streak INTEGER, exhaust INTEGER,
    stickiness REAL, ema_gap REAL, ema_check TEXT,
    UNIQUE(code, level, t)
    )
    """
    )
    conn.commit()
    finally:
    conn.close()

    # ———- 落库(INSERT OR REPLACE 幂等写入) ———-
    def save_macd(code, level, df):
    if df.empty:
    return
    conn = sqlite3.connect(DB_PATH)
    try:
    data = df.copy()
    data.insert(0, "level", level)
    data.insert(0, "code", str(code))
    data["t"] = data["t"].astype(str)
    data["collected_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    data = data[MACD_COLUMNS]
    data.to_sql("tmp_macd", conn, if_exists="replace", index=False)
    cols = ", ".join(MACD_COLUMNS)
    conn.execute(f"INSERT OR REPLACE INTO stock_macd ({cols}) SELECT {cols} FROM tmp_macd")
    conn.execute("DROP TABLE IF EXISTS tmp_macd")
    conn.commit()
    logger.info(f"落库MACD {code} {level} 共 {len(data)} 条")
    except Exception as e:
    logger.error(f"落库MACD失败:{str(e)}")
    finally:
    conn.close()

    def save_signal(code, level, sig):
    if sig.empty:
    return
    conn = sqlite3.connect(DB_PATH)
    try:
    data = sig.copy()
    data.insert(0, "level", level)
    data.insert(0, "code", str(code))
    data["t"] = data["t"].astype(str)
    data = data[SIGNAL_COLUMNS]
    data.to_sql("tmp_signal", conn, if_exists="replace", index=False)
    cols = ", ".join(SIGNAL_COLUMNS)
    conn.execute(f"INSERT OR REPLACE INTO macd_signal ({cols}) SELECT {cols} FROM tmp_signal")
    conn.execute("DROP TABLE IF EXISTS tmp_signal")
    conn.commit()
    logger.info(f"落库MACD信号 {code} {level} 共 {len(data)} 条")
    except Exception as e:
    logger.error(f"落库MACD信号失败:{str(e)}")
    finally:
    conn.close()

    # ———- 演示入口 ———-
    def demo(codes=None, level="d", start="20240101", end="20241231",
    exhaust_window=20):
    init_db()
    if codes is None:
    codes = ["600519", "000001", "300750"]
    for code in codes:
    rows = fetch_history_macd(code, level=level, adjust="n",
    start=start, end=end, limit=250)
    df = clean_macd_frame(rows)
    if df.empty:
    logger.warning(f"{code} MACD数据为空,跳过")
    continue
    sig = build_signal_frame(df, exhaust_window)
    save_macd(code, level, df)
    save_signal(code, level, sig)

    last = sig.iloc[–1]
    print(f"\\n=== {code} 级别 {level} ===")
    print(f"最新时间 {last['t']}")
    print(f"DIF(diff) {last['diff']} | DEA(dea) {last['dea']} | 柱(macd) {last['macd']}")
    print(f"EMA12 {last['ema12']} | EMA26 {last['ema26']} | 差值 {last['ema_gap']} | {last['ema_check']}")
    print(f"金叉死叉 {last['cross']}(1 金叉 / -1 死叉 / 0 无 / NULL 首根)")
    print(f"零轴分区 {last['zone']} | 柱符号 {last['bar_sign']}(1 红柱 / -1 绿柱)")
    print(f"同号连续 {last['bar_streak']} 根 | 柱体变化 {last['bar_trend']}")
    print(f"动能持续 {last['momentum_streak']} 根 | 动能衰竭 {last['exhaust']}(1 衰竭 / 0 否 / NULL 样本不足)")
    print(f"双线粘合度 {last['stickiness']}(越小越接近变盘)")
    golden = int((sig["cross"] == 1).sum())
    dead = int((sig["cross"] == –1).sum())
    up_bars = int((sig["zone"] == "零轴上方多头区").sum())
    down_bars = int((sig["zone"] == "零轴下方空头区").sum())
    exhaust_bars = int(sig["exhaust"].fillna(0).sum())
    print(f"区间内金叉 {golden} 次 | 死叉 {dead} 次")
    print(f"零轴上方 {up_bars} 根 | 零轴下方 {down_bars} 根 | 动能衰竭 {exhaust_bars} 根")
    print("\\n【仅为数据演示,不构成投资建议】已采集并落库 MACD 数据")

    if __name__ == "__main__":
    demo()

    代码运行后,每只股票会依次打印:最新一根 bar 的时间、DIF/DEA/柱值、两条 EMA 及其差值与共振验证、金叉死叉标记、零轴分区、柱符号与同号连续根数、柱体缩放、动能持续根数、动能衰竭标记、双线粘合度,最后输出区间内的金叉/死叉次数与零轴分区、动能衰竭的 bar 根数。所有数值均由实际接口返回计算得出,本文不预设任何具体结果。

    六、业务关键点

  • 接口不给收盘价,背离只能做指标层面近似:hsstock/history/macd 只返回 diff、dea、macd、ema12、ema26,没有原始收盘价。教科书里「价格创新高、MACD 不创新高」的顶背离必须价格参与,本文明确改用「diff 创窗口新高、但 |macd| 小于窗口内柱值峰值」来实现,并在函数名与注释里反复标注这是指标层面的近似。若确实需要严格背离,收盘价应从其他接口另行获取,再按 t 与本表做时间对齐,本文不做超出文档的字段假设。
  • 金叉死叉依赖严格的时间升序:判定用的是「上一根与当前根的位置关系」,因此清洗链里必须 sort_values("t") 升序并 drop_duplicates(subset=["t"], keep="last") 去重。时间乱序会造出大量假金叉假死叉;第一根 bar 返回 None 并落库为 NULL,与「无信号 0」严格区分,后续统计时不会被误计。
  • 零轴分区与金叉死叉是两套信号:分区只看 diff、dea 相对零轴的正负(同正为多头区、同负为空头区、一正一负为穿越区),金叉死叉看的是两线的相对位置。二者可以任意组合,例如「零轴上方多头区 + 金叉」与「零轴下方空头区 + 金叉」的含义完全不同,代码里分别落库便于后续自由组合筛选。
  • 柱体比较必须用绝对值:柱值 macd 正负只代表红绿方向,长度才代表力量。因此「放大/缩小」与「连续放大计数」都基于 |macd|;符号的连续性(bar_streak)与长度的连续性(momentum_streak)是两个独立维度,一根红柱可以「同号连续 8 根」但「动能持续 0 根」(说明柱体已在缩短)。
  • 动能衰竭窗口要有最小样本防御:judge_exhaust 在窗口内有效样本为空时返回 None 而非 0。窗口默认 20 根,上市不足、区间过短时窗口会被自动截断到 max(0, idx – window),此时宁可不判定,也不要用 1–2 根数据草率下「衰竭」结论。
  • 双线粘合度在 diff 接近 0 时会失真:粘合度 |diff – dea| / |diff| 的分母是 |diff|,当 diff 自身趋近 0 时,一个极小的分子也会算出很大的比例,甚至分母为 0。代码里 diff == 0 直接返回 None;实战中若要用该值筛选「粘合」,建议叠加 |diff| 的下限过滤,避免把零轴附近的噪音当成粘合。
  • 级别与除权要配套:分钟级(5/15/30/60)仅支持 n(不复权),代码里用 MINUTE_LEVELS 做了强制降级;日级及以上可用 f/b/fr/br。同一只股票不同除权方式的指标数值不同,落库唯一键是 UNIQUE(code, level, t),若要用前复权建议把除权方式并入 level 维度(如 d_f),避免互相覆盖。
  • null 值防御贯穿全程:停牌期间、上市不足、区间过短,五个字段都可能返回 null。所有函数统一先 pd.isna 判空,分母(|diff|)单独判零,绝不让 None 参与比较与四则运算。
  • 七、拓展练习

  • 扫描自选股池,筛选「zone = 零轴上方多头区 且 cross = 1 且 ema_check = 多头共振」的金叉样本,统计其后 5 根 bar 内 bar_trend 出现「缩小」的比例,检验「零轴上方金叉」的动能衰减速度。
  • 把「exhaust = 1 且 momentum_streak = 0」的 bar 单独抽出来,与随后 3 根内出现 cross = -1 的次数做交叉统计,验证本文这套「指标层面近似」的动能衰竭标记对死叉是否具备先行性(记得它不等于严格价格背离)。
  • 八、下篇预告

    下一篇《自研指标:量价背离、换手异动与振幅》将把视角拉回「接口没给你、必须自己算」的战场——用 hsstock/real/time 的实时字段(最新价 p、昨收 yc、成交量 v、成交额 cje、换手率 tr、振幅 zf、涨跌幅 pc)自研「量价背离」「换手异动 Z-Score」「振幅异常」三个实战指标,彻底摆脱对官方指标接口的依赖。

    九、免责申明

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

    赞(0)
    未经允许不得转载:171主机测评 » 量化数据进阶:多维实战篇(第 8 篇):MACD:金叉死叉与柱状动能
    分享到: 更多 (0)

    评论 抢沙发

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