本节摘要:QuantDinger 用 Python 写指标,形态收敛为三种(官方口径):overlay——与价格同坐标的连续叠加线;marker——落在特定 K 线上的离散事件点;band——上下轨夹出的区间带。本节先逐一给出定义、输出结构与典型用例;再用一张对照 ASCII 图把三者放回同一张 K 线图,建立直觉;然后给一段 API 形态示意代码,展示三种输出各自长什么样(类名与方法签名以官方文档为准);接着讲本章最重要的工程原则「一处定义、两处使用」——图表上画的和回测里算的是同一个类的同一次计算,杜绝「图上好看、回测对不上」;最后给选型方法:按输出的时间语义(连续区间、离散时刻、成对边界)对号入座。
| 形态 | 输出结构 | 时间语义 | 典型用例(示意) |
|---|---|---|---|
| overlay | 与 K 线等长的数值序列 | 每根 K 线一个值,连续 | 均线类、ATR 类、动量类 |
| marker | (时间/序号, 标签) 的稀疏列表 | 只在事件发生处有值 | 交叉触发、突破确认、缺口标记 |
| band | 上下轨(可加中轨)的成对序列 | 每根 K 线一对边界 | 波动通道、超买超卖区间带 |
选型的唯一依据是你想要的输出在说什么:说「这根 K 线上某个量是多少」用 overlay;说「这件事在这根 K 线上发生了」用 marker;说「此刻的合理区间在哪儿」用 band。三种可以组合:一个波动率指标可以同时产出 band(通道)与 marker(收缩触发点),4.2 节正是这么做的。
三种形态同图(放大版) 价格 │ ● │ ● ● ╭ ─ ─ band 上轨 ─ ─ ╮ │ ● ● ┈┈┈┈ ● ┈ overlay:均线 ┈┈ ● │● ● ╰ ─ ─ band 下轨 ─ ─ ╯ │ ▲ ▲ │ └─ marker:事件点 └─ marker:事件点 └───────────────────────────────▶ 时间 overlay 每根 K 线都有值;marker 只在事件处;band 上下成对
# indicator_forms_demo.py —— 三种形态的 API 形态示意(教学示意,以官方文档为准) class SmaOverlay: """overlay:简单移动平均,返回与 K 线等长的序列(前段不足为 None)""" def __init__(self, period=20): self.period = period def compute(self, closes): values = [None] * len(closes) for i in range(len(closes)): if i + 1 >= self.period: window = closes[i + 1 - self.period : i + 1] values[i] = sum(window) / self.period return {"form": "overlay", "name": "SMA" + str(self.period), "values": values} class CrossMarker: """marker:快慢线交叉事件,返回稀疏的事件点列表""" def __init__(self, fast=10, slow=30): self.fast, self.slow = fast, slow def compute(self, closes): events = [] for i in range(self.slow, len(closes)): f_now = sum(closes[i + 1 - self.fast : i + 1]) / self.fast f_prev = sum(closes[i - self.fast : i]) / self.fast s_now = sum(closes[i + 1 - self.slow : i + 1]) / self.slow s_prev = sum(closes[i - self.slow : i]) / self.slow if f_prev <= s_prev and f_now > s_now: events.append({"index": i, "kind": "golden_cross"}) if f_prev >= s_prev and f_now < s_now: events.append({"index": i, "kind": "death_cross"}) return {"form": "marker", "name": "MA-Cross", "events": events} if __name__ == "__main__": demo = [float(x) for x in range(1, 41)] # 单调上升的演示序列 ov = SmaOverlay(5).compute(demo) mk = CrossMarker(3, 8).compute(demo) print(ov["form"], "末值:", round(ov["values"][-1], 2)) print(mk["form"], "事件数:", len(mk["events"]), mk["events"][:2])
读这段示意的三个观察点:第一,overlay 的序列长度恒等于输入长度、前段用 None 占位——下游永远知道「第几根 K 线对应什么」;第二,marker 是稀疏的,只携带发生位置与事件种类,不携带仓位含义;第三,两种形态互不越界——交叉marker不负责「该买多少」,那是第 5 章 sizing 的职责。
同一指标实现同时服务两个消费者:图表(人看)与回测(机器算)。这是平台级的一致性保障:
┌──▶ 图表渲染:画在 K 线图上,供人复盘 同一个指标类 ────┤ (同一份代码) └──▶ 回测引擎:逐根喂给策略,供机器计算
反例是散件时代的常见病:图表软件里拖出来的「SMA(20)」和回测脚本里手写的「SMA(20)」,参数同名、实现对不齐(前复权口径、闭合判定差一点),复盘结论与回测结论各自为政。「一处定义、两处使用」从机制上消灭这类分歧——你在图上看到的每一个 marker,回测里策略也能在同位置看到;策略在回测里踩过的每一个信号,图上也能指给你看。
三个想法,三个答案(自测后再对):
| 想法 | 输出在说什么 | 形态 |
|---|---|---|
| 「每根 K 线的真实波幅」 | 每根一个值 | overlay |
| 「价格突破近 20 根高点这件事」 | 发生/未发生 | marker |
| 「按波幅画出的正常波动范围」 | 一对边界 | band |
选错的代价:把事件写成 overlay,序列里塞满「0 和 1」,图上难看、回测里还得再做一次判断;把边界写成 overlay,上下轨的对齐关系丢失。形态选对了,下游的一切消费都顺理成章。
词汇表齐了,该造一个真正的词。下一节从「波动率收缩后往往有行情展开」这个想法出发,写出规格、实现三件套(ATR、带宽分位、收缩事件)、跑通自测,再接入图表与回测。