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": [],
}
这个例子展示了完整的最小契约:
- 声明名称和描述。
- 用
df = df.copy()创建工作副本。 - 计算与 K 线等长的数据。
- 设置
output字典。
建议新指标按“先画一条线,再加参数,再加事件标记,最后才加复杂图层”的顺序开发。
2. 指标、策略和转换流程的边界
| 产物 | 负责内容 | 不负责内容 |
|---|---|---|
| Chart Indicator | 曲线、副图、灯带、视觉标记、区域、标签 | 回测、实盘、订单、仓位、杠杆、交易风控 |
| Strategy API V2 | 数据订阅、交易信号、订单意图、仓位、回测、实盘、保护规则 | 指标页的 output 图表渲染 |
| Indicator-to-Strategy | 把视觉信号的真实含义翻译成可执行策略 | 在原指标中混入下单逻辑 |
output["signals"] 只是图上的事件标记。例如 sell 类型的 “Death” 标记可以表示多头离场提醒,也可以表示行情转弱;它不会自动开空,更不会自动反手。
将只做多指标转换成策略时,通常采用:
- 明确的看多入场事件 →
open_long - 明确的看空离场事件 →
close_long - 只有用户明确要求做空,并提供独立的看空入场规则时,才生成
open_short
不要在指标里创建 open_long、close_long、open_short、close_short、add_long 或 reduce_long 等执行列,也不要使用旧式 # @strategy 注解。
3. 运行环境与输入数据
运行时预置:
df:当前图表的 pandas DataFrame,按时间从旧到新排列,每行对应一根 K 线。params:由参数声明和参数面板合并得到的字典。pd:预置的 pandas。np:预置的 numpy。open、high、low、close、volume:部分运行入口还提供同名便捷 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()。 - 核心序列计算优先使用
rolling、ewm、shift、where等向量化操作。
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 标识符。
- 类型只使用
int、float、bool、str或string。 - 注释中的默认值必须和
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必须是字典。plots或signals至少有一个键存在。- 每个
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、bar 或 circle;别名见下表 |
当前公开绘图类型:
| 写法 | 规范化结果 | 典型 Pine 对应 |
|---|---|---|
line | 折线 | plot(..., style=plot.style_line) |
bar、histogram、column | 从基准值绘制的柱 | histogram/columns |
circle、dot、point、scatter | 圆点 | circles/scatter |
lamp | 灯带圆点;同一副图有至少 3 个灯带序列时自动按行排列 | 状态面板的近似表达 |
样式字段按图形类型生效:
- 通用:
color、opacity。 - 折线:
lineWidth,以及lineStyle/style(例如solid、dashed)。 - 柱和圆点:
size/radius、borderColor、borderSize;柱还可设置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},
]
对象中的数值键可使用 value、y 或 data;颜色键可使用 color、fillColor 或 backgroundColor;大小键可使用 size、radius 或 r。推荐在新代码中只使用第一组规范字段 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通常为buy或sell,只控制标记方向,不是信号名称。text是稳定的信号名;可选textData可为每根 bar 提供不同标签。- 最稳定、最便于通知系统识别的写法,是用
data[i]中的有限非零价格激活第 i 根 bar 的信号。 text或textData本身不会激活信号。- 无信号位置必须使用真实的
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 换成 startPrice 和 endPrice。标签:
{
"type": "label",
"index": len(df) - 1,
"price": float(df["close"].iloc[-1]),
"text": "Trend Weakens",
"color": "#EF4444",
"textColor": "#FFFFFF",
}
索引写法对当前 df 最稳定。也支持与 K 线时间戳匹配的 startTime、endTime 和 time。
图层仍然只是视觉对象,不能表示真实订单、仓位或已托管止损。
10. 与 TradingView/Pine 的能力对应
QuantDinger 不执行 Pine 源码。迁移目标是保持指标含义、数值时序和视觉表达一致,再用 Python/pandas 实现。以下是当前契约的真实边界:
| TradingView/Pine 能力域 | 当前状态 | QuantDinger 写法或限制 |
|---|---|---|
| OHLCV、历史引用、滚动窗口 | 支持 | df、shift、rolling、ewm |
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 指标时逐项核对:
- 明确 Pine 版本、输入参数、源序列和指标所在周期。
- 对齐 SMA/EMA/RMA/WMA、标准差、ATR 和交叉等函数的精确定义;不要只比较函数名称。
- 对齐预热期、
na传播、除零处理和首次有效 bar。 - 把持续条件与一次性事件分开;Pine 的条件序列不能直接当成每根 bar 的重复通知。
- 用固定 OHLCV 样本比较序列容差、事件 bar 索引和图层锚点。
- Pine 脚本包含表中“尚未支持”的能力时,必须删减、近似表达,或先扩展平台契约,不能在指南或代码注释中声称完全一致。
Pine 能力分类可参考 TradingView 官方文档:Visuals overview、Inputs、Other timeframes and data、Bar 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 没有 rolling、shift、ewm、fillna 或 iloc。
优先使用 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。pd 与 np 已预置,通常无需 import。
禁止:
- 网络、文件、数据库和子进程访问;
eval、exec、compile、open;- 反射、动态导入、dunder 逃逸和沙箱绕过;
- pandas/numpy 的文件读取、写入和序列化方法;
os、sys、requests、socket、subprocess、threading、sqlite3、pathlib、pickle、ctypes、operator等模块。
指标验证有超时限制。避免无界循环、递归爆炸和逐行执行的高复杂度算法。
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": [],
}
逐步理解:
- 参数声明决定参数面板和搜索范围。
params.get读取与声明完全一致的默认值。- 两条 EMA 是持续状态,因此放进 plots。
- 金叉和死叉是一次性事件,因此经过
edge后放进 signals。 - 空标记使用
None。 - 指标明确把死叉命名为 “Long Exit”,避免转换策略时误解成开空。
15. 验证、调试和常见错误
建议每次按以下顺序:
- 保存版本。
- 运行/预览指标。
- 执行代码验证。
- 检查图上起始空值、极端行情和短数据区间。
- 修改参数,确认参数确实影响结果。
- 检查信号是否只在事件 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 | 检测到未来数据 | 改为只使用当前和历史数据 |
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。
- 短数据、预热区和异常参数不会导致崩溃。
- 已保存版本并通过预览与验证。