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": [],
}
这个例子展示了完整的最小契约:
- 声明名称和描述。
- 用
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,也可由当前渲染器支持其他样式 |
示例:
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 都重复标记。
把状态转换成边沿事件:
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. 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() |
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。pd 与 np 已预置,通常无需 import。
禁止:
- 网络、文件、数据库和子进程访问;
eval、exec、compile、open;- 反射、动态导入、dunder 逃逸和沙箱绕过;
- pandas/numpy 的文件读取、写入和序列化方法;
os、sys、requests、socket、subprocess、threading、sqlite3、pathlib、pickle、ctypes、operator等模块。
指标验证有超时限制。避免无界循环、递归爆炸和逐行执行的高复杂度算法。
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": [],
}
逐步理解:
- 参数声明决定参数面板和搜索范围。
params.get读取与声明完全一致的默认值。- 两条 EMA 是持续状态,因此放进 plots。
- 金叉和死叉是一次性事件,因此经过
edge后放进 signals。 - 空标记使用
None。 - 指标明确把死叉命名为 “Long Exit”,避免转换策略时误解成开空。
14. 验证、调试和常见错误¶
建议每次按以下顺序:
- 保存版本。
- 运行/预览指标。
- 执行代码验证。
- 检查图上起始空值、极端行情和短数据区间。
- 修改参数,确认参数确实影响结果。
- 检查信号是否只在事件 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。
- [ ] 短数据、预热区和异常参数不会导致崩溃。
- [ ] 已保存版本并通过预览与验证。
