QuantDingerQuantDingerv5.0.1 Docs 文档中心
中 / EN GitHub

QuantDinger 指标开发指南

适用范围:当前 QuantDinger 图表指标契约 面向读者:第一次编写指标的用户、从 Pine/通达信公式迁移的开发者,以及需要审查 AI 生成代码的维护者

QuantDinger 指标是运行在指标编辑器中的 Python 图表程序。它读取当前图表的 K 线数据,计算序列,并通过 output 返回曲线、标记和稀疏图层。

最重要的边界是:指标只负责看图,不负责交易执行。

指标不能下单、回测、运行实盘、读取账户、管理仓位、设置杠杆或执行止盈止损。需要交易时,应先把指标中的视觉信号转换成 Strategy API V2 策略,再在策略页完成验证、回测和部署。


1. 先完成一个最小指标

把下面代码粘贴到指标编辑器并运行:

my_indicator_name = "Close Line"
my_indicator_description = "Displays the close price as a chart overlay."

df = df.copy()

close_line = [
    None if pd.isna(value) else float(value)
    for value in df["close"]
]

output = {
    "name": my_indicator_name,
    "plots": [
        {
            "name": "Close",
            "data": close_line,
            "color": "#3B82F6",
            "type": "line",
            "overlay": True,
        }
    ],
    "signals": [],
    "layers": [],
}

这个例子展示了完整的最小契约:

  1. 声明名称和描述。
  2. df = df.copy() 创建工作副本。
  3. 计算与 K 线等长的数据。
  4. 设置 output 字典。

建议新指标按“先画一条线,再加参数,再加事件标记,最后才加复杂图层”的顺序开发。


2. 指标、策略和转换流程的边界

产物 负责内容 不负责内容
Chart Indicator 曲线、副图、灯带、视觉标记、区域、标签 回测、实盘、订单、仓位、杠杆、交易风控
Strategy API V2 数据订阅、交易信号、订单意图、仓位、回测、实盘、保护规则 指标页的 output 图表渲染
Indicator-to-Strategy 把视觉信号的真实含义翻译成可执行策略 在原指标中混入下单逻辑

output["signals"] 只是图上的事件标记。例如 sell 类型的 “Death” 标记可以表示多头离场提醒,也可以表示行情转弱;它不会自动开空,更不会自动反手。

将只做多指标转换成策略时,通常采用:

  • 明确的看多入场事件 → open_long
  • 明确的看空离场事件 → close_long
  • 只有用户明确要求做空,并提供独立的看空入场规则时,才生成 open_short

不要在指标里创建 open_longclose_longopen_shortclose_shortadd_longreduce_long 等执行列,也不要使用旧式 # @strategy 注解。


3. 运行环境与输入数据

运行时预置:

  • df:当前图表的 pandas DataFrame,按时间从旧到新排列,每行对应一根 K 线。
  • params:由参数声明和参数面板合并得到的字典。
  • pd:预置的 pandas。
  • np:预置的 numpy。
  • openhighlowclosevolume:部分运行入口还提供同名便捷 Series;为了可读性和可移植性,教程建议优先使用 df["close"]

标准 OHLCV 字段:

open_price = df["open"]
high = df["high"]
low = df["low"]
close = df["close"]
volume = df["volume"]

注意:

  • 不要假设一定存在 time 列,时间也可能已经在 DataFrame 索引中。
  • 不要重命名或删除 OHLCV 列。
  • 可选字段必须先检查,例如 if "turnover" in df.columns:
  • 修改 DataFrame 前先执行 df = df.copy()
  • 核心序列计算优先使用 rollingewmshiftwhere 等向量化操作。

4. 文件结构与元数据

推荐结构:

# @param period int 20 Calculation period

my_indicator_name = "Example Indicator"
my_indicator_description = "Explains what is drawn and how events are marked."

df = df.copy()

period = int(params.get("period", 20))

# Helper functions
# Series calculation
# Marker construction

output = {
    "name": my_indicator_name,
    "plots": [],
    "signals": [],
    "layers": [],
}

每个指标都应声明:

my_indicator_name = "Dual EMA Viewer"
my_indicator_description = "Chart-only EMA crossover indicator with visual event markers."

名称应简短稳定;描述应说明:

  • 计算了什么;
  • 在主图还是副图显示;
  • 标记代表什么事件;
  • 有哪些重要参数。

不要在描述中承诺收益或暗示已通过实盘验证。

根据项目源码规则,代码标识符、元数据、注释、参数描述和默认显示标签使用英文。中文可以用于本教程正文;只有用户明确要求本地化显示标签时,指标中的展示文本才使用指定语言。


5. 参数声明与参数面板

语法:

# @param <name> <int|float|bool|str> <default> <description>

示例:

# @param fast_len int 12 Fast EMA period
# @param slow_len int 26 Slow EMA period
# @param band_pct float 1.5 Channel width percent
# @param show_marks bool true Show crossover markers
# @param source str close Price source

声明不会自动创建 Python 变量,必须显式读取:

fast_len = int(params.get("fast_len", 12))
slow_len = int(params.get("slow_len", 26))
band_pct = float(params.get("band_pct", 1.5))
show_marks = bool(params.get("show_marks", True))
source = str(params.get("source", "close"))

硬性规则:

  • 每行只声明一个参数。
  • 参数名使用合法 Python 标识符。
  • 类型只使用 intfloatboolstrstring
  • 注释中的默认值必须和 params.get 的回退值一致。
  • 布尔值在声明中写 true/false,Python 中写 True/False
  • 字符串默认值不能包含空格,因为解析器把默认值读取为一个 token。

参数搜索可在描述末尾声明候选范围:

# @param period int 20 Lookback period range=5:60:5
# @param multiplier float 2.0 Band multiplier values=1.5,2.0,2.5,3.0

range=start:end:step 是包含终点的等差候选序列;values=a,b,c 是显式候选列表。单个参数最多展开 1024 个候选值。描述中的范围标记会从用户可见描述中移除。

指标参数只控制计算和显示。不要声明账户、标的、周期、仓位、杠杆、止损或止盈等执行参数。


6. output 输出契约

指标运行结束时必须设置字典类型的 output

output = {
    "name": my_indicator_name,
    "plots": plots,
    "signals": signals,
    "layers": layers,
}

可选字段:

output["calculatedVars"] = {}

验证要求:

  • output 必须是字典。
  • plotssignals 至少有一个键存在。
  • 每个 plot["data"] 的长度必须等于 len(df)
  • 每个 signal["data"] 的长度必须等于 len(df)
  • 序列中不要输出 NaN、正无穷或负无穷;缺失点使用 None
  • layers 不需要逐 bar 数组,但索引、时间和价格必须落在当前数据的有效语义范围内。

推荐总是显式提供空列表,这样结构最清晰:

output = {
    "name": my_indicator_name,
    "plots": [],
    "signals": [],
    "layers": [],
}

7. plots:主图曲线与副图序列

每个 plot 至少包含:

字段 类型 含义
name str 图例和序列名称
data list df 等长的数值/None 列表
color str 推荐使用 #RRGGBB
overlay bool True 主图,False 副图
type str,可选 常用 line,也可由当前渲染器支持其他样式

示例:

plots = [
    {
        "name": "EMA Fast",
        "data": fast_values,
        "color": "#22C55E",
        "type": "line",
        "overlay": True,
    },
    {
        "name": "RSI",
        "data": rsi_values,
        "color": "#8B5CF6",
        "type": "line",
        "overlay": False,
    },
]

价格均线、布林带和通道通常使用主图;RSI、MACD 和状态灯带通常使用副图。

统一处理空值:

def to_plot_list(series):
    return [
        None if pd.isna(value) else float(value)
        for value in series
    ]

不要把价格叠加线的预热空值填成 0,否则图上会出现从零点拉到真实价格的误导性线段。


8. signals:稀疏视觉事件

signal 示例:

signals = [
    {
        "type": "buy",
        "text": "Long Entry",
        "color": "#22C55E",
        "data": entry_marks,
    },
    {
        "type": "sell",
        "text": "Long Exit",
        "color": "#EF4444",
        "data": exit_marks,
    },
]

规则:

  • type 通常为 buysell,只控制标记方向,不是信号名称。
  • text 是稳定的信号名;可选 textData 可为每根 bar 提供不同标签。
  • 只有 data[i] 中的有限数值会激活第 i 根 bar 的信号。
  • texttextData 本身不会激活信号。
  • 无信号位置必须使用真实的 None
  • 默认标记一次性事件,不要在条件持续为真时每根 bar 都重复标记。

把状态转换成边沿事件:

def edge(condition):
    current = condition.fillna(False).astype(bool)
    previous = current.shift(1, fill_value=False).astype(bool)
    return current & ~previous

生成价格标记:

entry_event = edge(ema_fast > ema_slow)
exit_event = edge(ema_fast < ema_slow)

entry_marks = [
    float(df["low"].iloc[i] * 0.995)
    if bool(entry_event.iloc[i])
    else None
    for i in range(len(df))
]

exit_marks = [
    float(df["high"].iloc[i] * 1.005)
    if bool(exit_event.iloc[i])
    else None
    for i in range(len(df))
]

如果要求“确认后下一根显示”:

confirmed_entry = edge(raw_entry).shift(
    1,
    fill_value=False,
).astype(bool)

这只是把已确认事件向后移动一根,并没有读取未来数据。


9. layers:区域、线段和标签

普通指标优先使用 plots 和 signals。只有在供需区、支撑阻力、通道、失效位或结构标签确实能提高可读性时才使用 layers。

区域:

{
    "type": "zone",
    "startIndex": 120,
    "endIndex": 180,
    "top": 105.2,
    "bottom": 101.8,
    "text": "Demand",
    "fillColor": "#22C55E",
    "borderColor": "#22C55E",
    "opacity": 0.12,
}

水平线:

{
    "type": "line",
    "startIndex": 100,
    "endIndex": len(df) - 1,
    "price": 98.5,
    "text": "Support",
    "color": "#F59E0B",
    "dashed": True,
}

斜线把 price 换成 startPriceendPrice。标签:

{
    "type": "label",
    "index": len(df) - 1,
    "price": float(df["close"].iloc[-1]),
    "text": "Trend Weakens",
    "color": "#EF4444",
    "textColor": "#FFFFFF",
}

索引写法对当前 df 最稳定。也支持与 K 线时间戳匹配的 startTimeendTimetime

图层仍然只是视觉对象,不能表示真实订单、仓位或已托管止损。


10. pandas 与 numpy 类型陷阱

最常见的错误是把 numpy ndarray 当成 pandas Series。

错误:

values = np.where(close > close.shift(1), close, 0)
average = values.rolling(10).mean()

np.where 可能返回 ndarray,而 ndarray 没有 rollingshiftewmfillnailoc

优先使用 pandas 原生写法:

values = close.where(close > close.shift(1), 0)
average = values.rolling(10).mean()

必须包装 ndarray 时:

array = np.where(close > close.shift(1), close, 0)
values = pd.Series(array, index=df.index)

一定传入 index=df.index。否则新 Series 使用 RangeIndex,与 DatetimeIndex 数据做运算时会静默错位。

常用替换:

numpy 写法 pandas 优先写法
np.where(cond, a, b) a.where(cond, b)
np.maximum(s, 0) s.clip(lower=0)
np.minimum(s, k) s.clip(upper=k)
np.abs(s) s.abs()

11. 避免未来数据和重绘

指标只能使用当前及历史 bar。禁止:

  • shift(-1)shift(-N)
  • 循环中的 iloc[i + 1]
  • bars_ago(-N)
  • rolling(..., center=True)
  • 用完整数据集最后一行反向修改历史信号;
  • 任何利用未来最高价、最低价或未来确认结果标记过去 bar 的写法。

合法确认通常使用当前条件与上一根状态:

cross_up = (
    (ema_fast > ema_slow)
    & (ema_fast.shift(1) <= ema_slow.shift(1))
)

如果信号必须等当前 bar 收盘才能确定,转换成策略后应在下一根 bar 执行,不要为了让图形更漂亮而把信号提前。


12. 沙箱与安全限制

允许的计算模块包括 numpy、pandas、math、json、datetime、time、collections、functools、itertools、statistics、decimal、fractions 和 copy。pdnp 已预置,通常无需 import。

禁止:

  • 网络、文件、数据库和子进程访问;
  • evalexeccompileopen
  • 反射、动态导入、dunder 逃逸和沙箱绕过;
  • pandas/numpy 的文件读取、写入和序列化方法;
  • ossysrequestssocketsubprocessthreadingsqlite3pathlibpicklectypesoperator 等模块。

指标验证有超时限制。避免无界循环、递归爆炸和逐行执行的高复杂度算法。


13. 完整教程:双 EMA 交叉指标

# @param fast_len int 12 Fast EMA period range=5:30:1
# @param slow_len int 26 Slow EMA period range=10:80:2
# @param confirm_next_bar bool false Show markers one bar after confirmation
# @param show_marks bool true Show crossover markers

my_indicator_name = "Dual EMA Viewer"
my_indicator_description = "Displays two EMAs and marks confirmed crossover events."

df = df.copy()

fast_len = int(params.get("fast_len", 12))
slow_len = int(params.get("slow_len", 26))
confirm_next_bar = bool(params.get("confirm_next_bar", False))
show_marks = bool(params.get("show_marks", True))

close = df["close"]
high = df["high"]
low = df["low"]


def edge(condition):
    current = condition.fillna(False).astype(bool)
    previous = current.shift(1, fill_value=False).astype(bool)
    return current & ~previous


def to_plot_list(series):
    return [
        None if pd.isna(value) else float(value)
        for value in series
    ]


ema_fast = close.ewm(span=fast_len, adjust=False).mean()
ema_slow = close.ewm(span=slow_len, adjust=False).mean()

golden = edge(ema_fast > ema_slow)
death = edge(ema_fast < ema_slow)

if confirm_next_bar:
    golden = golden.shift(1, fill_value=False).astype(bool)
    death = death.shift(1, fill_value=False).astype(bool)

golden_marks = [
    float(low.iloc[i] * 0.995)
    if show_marks and bool(golden.iloc[i])
    else None
    for i in range(len(df))
]

death_marks = [
    float(high.iloc[i] * 1.005)
    if show_marks and bool(death.iloc[i])
    else None
    for i in range(len(df))
]

output = {
    "name": my_indicator_name,
    "plots": [
        {
            "name": "EMA Fast",
            "data": to_plot_list(ema_fast),
            "color": "#22C55E",
            "type": "line",
            "overlay": True,
        },
        {
            "name": "EMA Slow",
            "data": to_plot_list(ema_slow),
            "color": "#3B82F6",
            "type": "line",
            "overlay": True,
        },
    ],
    "signals": [
        {
            "type": "buy",
            "text": "Long Entry",
            "color": "#22C55E",
            "data": golden_marks,
        },
        {
            "type": "sell",
            "text": "Long Exit",
            "color": "#EF4444",
            "data": death_marks,
        },
    ],
    "layers": [],
}

逐步理解:

  1. 参数声明决定参数面板和搜索范围。
  2. params.get 读取与声明完全一致的默认值。
  3. 两条 EMA 是持续状态,因此放进 plots。
  4. 金叉和死叉是一次性事件,因此经过 edge 后放进 signals。
  5. 空标记使用 None
  6. 指标明确把死叉命名为 “Long Exit”,避免转换策略时误解成开空。

14. 验证、调试和常见错误

建议每次按以下顺序:

  1. 保存版本。
  2. 运行/预览指标。
  3. 执行代码验证。
  4. 检查图上起始空值、极端行情和短数据区间。
  5. 修改参数,确认参数确实影响结果。
  6. 检查信号是否只在事件 bar 出现。
提示或错误 原因 修复
EMPTY_CODE 代码为空 提供完整指标源码
MISSING_OUTPUT 没有设置 output 添加字典输出
MissingOutput 执行后未得到输出变量 检查分支和变量作用域
InvalidOutputType output 不是字典 改为 dict
InvalidOutputStructure plots/signals 键都不存在 至少提供其中一个
LengthMismatch 序列长度与 K 线不一致 让每个 data 等于 len(df)
MISSING_DF_COPY 缺少工作副本 在计算前添加 df = df.copy()
PARAM_DEFAULT_MISMATCH 声明和读取默认值不同 对齐两个默认值
DECLARED_PARAMS_NOT_READ_VIA_PARAMS_GET 声明后没有读取 显式调用 params.get
EXECUTION_COLUMNS_IGNORED_FOR_INDICATOR 指标中写了交易执行列 删除并转换成 V2 策略
STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR 使用旧策略注解 删除旧注解
NDARRAY_PANDAS_METHOD_MISUSE ndarray 被当作 Series 用 pandas 写法或包装并保留索引
FUTURE_DATA_LEAK 检测到未来数据 改为只使用当前和历史数据

15. 转换成策略前的语义清单

转换前明确回答:

  • 哪个标记是多头入场?
  • 哪个标记是多头离场?
  • 是否真的需要做空?看空离场不能自动等同于做空入场。
  • 是否需要反手?如果需要,平仓和反向开仓是否为两个独立动作?
  • 信号在哪个周期、哪根已收盘 bar 确认?
  • 是否允许重复入场、加仓或减仓?
  • 仓位大小、止损、止盈和追踪止损如何定义?
  • 交易标的和市场类型是什么?

转换后应删除图表专用的颜色、标签偏移、plots、layers 和 marker 数组,保留信号代数,并用 Strategy API V2 明确声明标的、周期、仓位和风险。

生成的策略必须重新验证和回测。发布到市场前,系统要求至少有一条成功回测记录。


16. 发布前检查清单

  • [ ] 名称和描述存在,且不包含收益承诺。
  • [ ] 代码注释、标识符、元数据和默认标签为英文。
  • [ ] df = df.copy() 已执行。
  • [ ] 每个参数都通过 params.get 读取,默认值一致。
  • [ ] 不包含订单、仓位、杠杆或交易风控代码。
  • [ ] output 是字典。
  • [ ] 每个 plot/signal data 长度都等于 len(df)
  • [ ] 缺失值使用 None,没有 NaN 或无穷值输出。
  • [ ] signals 表示稀疏事件,持续状态放在 plots 或少量 layers。
  • [ ] 不读取未来数据。
  • [ ] numpy 结果在调用 pandas 方法前已转换成带正确索引的 Series。
  • [ ] 短数据、预热区和异常参数不会导致崩溃。
  • [ ] 已保存版本并通过预览与验证。