QuantDinger 指标开发指南

August 1, 2026 · View on GitHub

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

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

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

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

与 TradingView/Pine 的兼容目标是:覆盖常用单标的、单周期指标的计算与视觉语义,而不是兼容 Pine 语法、全部 API 或逐 tick 执行模型。 从 Pine 迁移时需要把公式改写为 Python/pandas,并按本文的 output 契约返回结果。


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 至少包含:

字段类型含义
namestr图例和序列名称
datalistdf 等长的数值/None 列表
colorstr推荐使用 #RRGGBB
overlayboolTrue 主图,False 副图
typestr,可选linebarcircle;别名见下表

当前公开绘图类型:

写法规范化结果典型 Pine 对应
line折线plot(..., style=plot.style_line)
barhistogramcolumn从基准值绘制的柱histogram/columns
circledotpointscatter圆点circles/scatter
lamp灯带圆点;同一副图有至少 3 个灯带序列时自动按行排列状态面板的近似表达

样式字段按图形类型生效:

  • 通用:coloropacity
  • 折线:lineWidth,以及 lineStyle/style(例如 soliddashed)。
  • 柱和圆点:size/radiusborderColorborderSize;柱还可设置 baseValue
  • 同一指标内所有 overlay=False 的序列共享一个副图,不会像多个独立 Pine 脚本那样自动创建多个副图。

需要 Pine 的 series color/动态点大小时,data 的单点可以从数值扩展为对象:

dynamic_points = [
    None,
    {"value": 102.5, "color": "#22C55E", "size": 4},
    {"value": 101.8, "color": "#EF4444", "size": 6},
]

对象中的数值键可使用 valueydata;颜色键可使用 colorfillColorbackgroundColor;大小键可使用 sizeradiusr。推荐在新代码中只使用第一组规范字段 value/color/size

示例:

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 都重复标记。

兼容格式也允许 True,或 {"active": True, "price": ..., "text": ..., "color": ...}。布尔值没有价格时,渲染器会按当前 K 线高低点定位。为了让图表预览、保存后的信号监控和未来客户端保持一致,新指标优先使用“价格或 None”格式;只有确实需要逐点文本/颜色时才使用对象。

renderMode 控制持续条件如何变成标记:

行为
events(显式事件模式)每个非空事件点都显示并可触发通知
points / markers / raw明确保留每个激活点
state / continuous / condition只在 False → True 的边沿显示并通知

如果不指定模式,运行时会把高密度激活序列识别为持续状态并自动做边沿化。公共指标不要依赖这个启发式规则;应直接生成稀疏事件,或显式设置 renderMode

把状态转换成边沿事件:

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. 与 TradingView/Pine 的能力对应

QuantDinger 不执行 Pine 源码。迁移目标是保持指标含义、数值时序和视觉表达一致,再用 Python/pandas 实现。以下是当前契约的真实边界:

TradingView/Pine 能力域当前状态QuantDinger 写法或限制
OHLCV、历史引用、滚动窗口支持dfshiftrollingewm
input.int/float/bool/string支持主要语义# @param;暂不支持 Pine 的 group、inline、confirm 和专用 color/source/timeframe/symbol 输入控件
ta.* 常用技术指标计算语义可实现使用 pandas/numpy;指标运行时暂不提供与 Pine 同名的 ta 命名空间,迁移时必须核对平滑算法、预热值和 NaN 规则
plot()、histogram、circles支持常用子集plots 的 line/bar/circle,支持主图/副图和逐点颜色/大小
plotshape()plotchar()支持主要事件语义使用 signals;动态文字用 textData 或点对象
hline()、line、box、label支持常用静态结果使用 line/zone/label layers;每次运行返回最终对象列表,没有 Pine 对象 ID、修改方法或垃圾回收语义
alert()alertcondition()部分支持指标输出 signals,保存后由用户配置监控通知;指标代码本身不直接发送通知或下单
arrays、maps、用户函数与状态计算Python 等价能力使用 list/dict、函数、pandas Series;这是整段 DataFrame 批量计算,不是 Pine 的逐 bar var/varip 执行模型
fill()、linefill、bgcolor()barcolor()尚无一等契约固定区间可用 zone 近似;不能宣称为完整填充、背景或 K 线着色兼容
area/step/cross/arrow、plotcandle()/plotbar()尚未支持当前未知 type 会退化为 line,不应依赖退化行为
table、polyline、动态 drawing 对象生命周期尚未支持不要输出未定义对象
request.security()request.*()、跨标的/多周期尚未支持指标只接收当前图表 DataFrame;不能在代码中自行请求网络或交易所数据
barstate.*、逐 tick 更新和 rollback不同执行模型当前按收到的完整 DataFrame 批量重算;应以已完成 K 线事件作为可复现基准
strategy.*、订单、仓位和回测不属于指标转换到 Strategy API V2

因此,“大部分功能一致”的推荐验收口径是:常见单标的、单周期指标在相同 OHLCV、参数、已完成 K 线上得到等价数值和事件;不要求 Pine 源码直接运行,也不把多周期请求、逐 tick 状态或高级绘图对象算作当前已兼容功能。

迁移 Pine 指标时逐项核对:

  1. 明确 Pine 版本、输入参数、源序列和指标所在周期。
  2. 对齐 SMA/EMA/RMA/WMA、标准差、ATR 和交叉等函数的精确定义;不要只比较函数名称。
  3. 对齐预热期、na 传播、除零处理和首次有效 bar。
  4. 把持续条件与一次性事件分开;Pine 的条件序列不能直接当成每根 bar 的重复通知。
  5. 用固定 OHLCV 样本比较序列容差、事件 bar 索引和图层锚点。
  6. Pine 脚本包含表中“尚未支持”的能力时,必须删减、近似表达,或先扩展平台契约,不能在指南或代码注释中声称完全一致。

Pine 能力分类可参考 TradingView 官方文档:Visuals overviewInputsOther timeframes and dataBar states


11. 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()

12. 避免未来数据和重绘

指标只能使用当前及历史 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 执行,不要为了让图形更漂亮而把信号提前。


13. 沙箱与安全限制

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

禁止:

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

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


14. 完整教程:双 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”,避免转换策略时误解成开空。

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

建议每次按以下顺序:

  1. 保存版本。
  2. 运行/预览指标。
  3. 执行代码验证。
  4. 检查图上起始空值、极端行情和短数据区间。
  5. 修改参数,确认参数确实影响结果。
  6. 检查信号是否只在事件 bar 出现。
提示或错误原因修复
EMPTY_CODE代码为空提供完整指标源码
MISSING_OUTPUT没有设置 output添加字典输出
MissingOutput执行后未得到输出变量检查分支和变量作用域
InvalidOutputTypeoutput 不是字典改为 dict
InvalidOutputStructureplots/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_MISUSEndarray 被当作 Series用 pandas 写法或包装并保留索引
FUTURE_DATA_LEAK检测到未来数据改为只使用当前和历史数据

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

转换前明确回答:

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

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

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


17. 发布前检查清单

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