这份指南不是单纯罗列接口,而是站在策略开发者视角,回答一个更实际的问题:
到底应该怎么写一个结构清晰、能回测、能落地成平台策略的指标策略?
QuantDinger 当前支持两条 Python 开发路径:
- IndicatorStrategy:基于
df的指标/信号脚本,用于 Indicator IDE、图表渲染和信号型回测。 - ScriptStrategy:基于
on_init / on_bar的事件驱动脚本,用于策略运行时、策略回测与实盘执行。
如果你要从零开始开发一个策略,默认建议是:
- 先用
IndicatorStrategy把信号逻辑跑通。 - 先验证图表、信号和回测语义。
- 只有当你需要运行时状态、动态仓位管理或执行控制时,再升级为
ScriptStrategy。
很多开发者会把信号逻辑、止盈止损、仓位管理、执行逻辑混在一起写,结果文档看不懂、代码也不好维护。
可以把 IndicatorStrategy 理解成:
- 基于
df计算指标序列 - 生成布尔型
buy/sell信号 - 通过元数据声明默认策略配置
- 返回
output供图表展示
它最适合:
- 指标研究
- 策略原型验证
- 参数调优
- 信号型回测
- 先做信号、后保存成平台策略的工作流
可以把 ScriptStrategy 理解成:
- 按 bar 逐根执行的运行时逻辑
- 通过
ctx.position读取当前持仓状态 - 用
ctx.buy()、ctx.sell()、ctx.close_position()发出动作 - 把退出、仓位、执行节奏写进代码
它最适合:
- 有状态的执行逻辑
- 动态止盈止损
- 分批加仓、减仓、部分止盈
- 冷却期、重入限制、bot 型执行策略
对于 IndicatorStrategy,请强制把逻辑拆成三层:
- 指标层:均线、RSI、ATR、布林带、过滤条件。
- 信号层:
df['buy']和df['sell']。 - 风险默认配置层:
# @strategy stopLossPct ...、takeProfitPct、entryPct等。
不要把这三层混成一团。
尤其要明确:
buy/sell负责表达什么时候进出场# @strategy负责表达引擎默认如何控风险、如何设仓位- 杠杆属于产品配置,不属于指标脚本
| 使用场景 | 推荐模式 |
|---|---|
| 写指标、叠加图表、画买卖点 | IndicatorStrategy |
| 研究 dataframe 上的进出场信号 | IndicatorStrategy |
| 只想给策略补固定止损、止盈、仓位默认值 | IndicatorStrategy |
| 需要逐根读取持仓状态做判断 | ScriptStrategy |
| 止损止盈依赖当前持仓状态动态变化 | ScriptStrategy |
| 需要分批开平仓、状态机、bot 风格执行 | ScriptStrategy |
一个简单判断方法:
- 如果你的逻辑可以表述成“条件 A 出现就买,条件 B 出现就卖”,先用
IndicatorStrategy - 如果你的逻辑更像“开仓后要持续盯着当前持仓,并根据状态做不同反应”,那就应该用
ScriptStrategy
这是大多数新策略最推荐的开发路径。
脚本开头先定义名称、描述、可调参数、默认策略配置。
my_indicator_name = "Trend Pullback Strategy"
my_indicator_description = "Buy pullbacks in an uptrend and exit on weakness."
# @param fast_len int 20 Fast EMA length
# @param slow_len int 50 Slow EMA length
# @param rsi_len int 14 RSI length
# @param rsi_floor float 45 Minimum RSI for long entries
# @strategy stopLossPct 0.03
# @strategy takeProfitPct 0.06
# @strategy entryPct 0.25
# @strategy trailingEnabled true
# @strategy trailingStopPct 0.02
# @strategy trailingActivationPct 0.04
# @strategy tradeDirection long# @param 用来定义用户经常调的参数。
格式如下:
# @param <name> <int|float|bool|str|string> <default> <描述>最佳实践:
- 声明后的参数,应该通过
params.get(...)读取 string与str等价- 如果声明了参数,却仍然把值硬编码在正文里,平台内置的代码质量检查会给出提醒
# @strategy 用来定义策略默认配置,比如:
stopLossPct:止损比例,例如0.03表示 3%takeProfitPct:止盈比例,例如0.06表示 6%entryPct:开仓资金占比trailingEnabledtrailingStopPcttrailingActivationPcttradeDirection:long、short或both
这里有个非常关键的边界:
- 这些是引擎读取的默认配置
- 不是让你再去 dataframe 里造一列
stop_loss - 不要在这里写
leverage - 数值尽量保持合理,并结合回测验证;底层解析器允许的范围会比示例更宽松
Indicator 代码运行在沙盒里,pd、np 和 params 字典已预置。
推荐开头固定写:
df = df.copy()通常可用列包括:
openhighlowclosevolume
time 列可能存在,但不要假设其类型永远一致。
避免这些写法:
- 网络请求
- 文件读写
- 子进程
eval、exec、open、__import__这类破坏沙盒边界的模式
回测引擎读取的是两列布尔信号:
df['buy']df['sell']
它们应满足:
- 与 dataframe 长度完全一致
fillna(False)后为布尔值- 除非你明确要连续触发,否则应尽量做成边缘触发
推荐模式:
raw_buy = (ema_fast > ema_slow) & (ema_fast.shift(1) <= ema_slow.shift(1))
raw_sell = (ema_fast < ema_slow) & (ema_fast.shift(1) >= ema_slow.shift(1))
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
df['sell'] = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)这样可以避免同一段趋势里每根 bar 都重复发信号。
止盈止损和仓位管理最容易在这里写乱。
在 IndicatorStrategy 里,退出逻辑通常有两种合法写法。
也就是由你的指标逻辑直接生成 df['sell']。
典型例子:
- 均线死叉
- RSI 跌破阈值
- 收盘价跌破 ATR 止损线
- 均值回归到目标位后离场
如果退出本身就是策略思想的一部分,用这种写法最自然。
也就是你只定义默认配置,由引擎按固定规则处理:
stopLossPcttakeProfitPctentryPct- trailing 系列参数
如果你的信号逻辑想保持简洁,而保护性规则是固定的,就用这种写法。
尽量明确一个“主退出来源”。
例如:
- 如果你的核心逻辑是“金叉进,死叉出”,那退出就主要由
sell信号负责 - 如果你的逻辑是“信号进场,固定 3% 止损 + 6% 止盈管理交易”,那退出主要由
# @strategy负责
两者可以同时存在,但一定要在注释或描述里写清楚,否则别的开发者不知道到底是信号退出还是引擎退出在起主要作用。
脚本最后必须赋值 output:
output = {
"name": "My Strategy",
"plots": [],
"signals": []
}主要支持键:
nameplotssignalscalculatedVars:可选元数据
每个 plot 项通常包含:
namedata,长度必须等于len(df)coloroverlay- 可选
type
每个 signal 项通常包含:
type:buy或selltextcolordata:无信号的 bar 用None
指标回测是典型的信号驱动:
- 引擎读取
df['buy']和df['sell'] - 信号按 bar close 确认
- 通常在下一根 bar 开盘价成交
这件事非常重要,因为:
- 你在当前 K 线上画出来的“止损线”不等于系统一定按这根 K 线内部价格成交
- 一旦用了
shift(-1),就基本等于引入未来函数
还要注意一个实现细节:
- 标准工作流下,最常见的成交语义仍然是“收盘确认、下一根开盘成交”
- 但保存后的策略快照,会根据产品配置被规范成
next_bar_open或same_bar_close - 如果你改了成交时机配置,不要凭印象判断结果,要重新回测并核对成交明细
这一节就是给开发者的直接答案。
如果你想要的是固定默认配置,就写成 # @strategy:
# @strategy stopLossPct 0.03
# @strategy takeProfitPct 0.06
# @strategy entryPct 0.25
# @strategy tradeDirection long含义分别是:
stopLossPct 0.03:默认 3% 止损takeProfitPct 0.06:默认 6% 止盈entryPct 0.25:默认用 25% 资金开仓tradeDirection long:默认只做多
这种写法适合:
- 信号代码尽量简单
- 希望回测时能直接读懂默认风险参数
- 希望 UI 和引擎都能直接识别这些默认值
如果你的“止损”本质上是策略逻辑的一部分,那就不要假装成外部配置,而是直接写进 sell 信号。
例如:跌破 ATR 风格止损线就卖出。
atr = (df['high'] - df['low']).rolling(14).mean()
stop_line = df['close'].rolling(20).max() - atr * 2.0
raw_sell = df['close'] < stop_line.shift(1)
df['sell'] = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)这种写法表示:
- 退出属于你的指标逻辑
- 引擎不是替你“发明”一个止损
- 你最好在描述或注释里说明这一点
对 IndicatorStrategy 来说,仓位管理应该尽量保持简单:
- 用
entryPct管默认开仓资金占比 - 用
tradeDirection管做多 / 做空 / 双向 - 用固定的止损止盈或 trailing 默认值做保护
如果你需要下面这些能力:
- 分批加仓、减仓
- 部分止盈
- 开仓前后用不同逻辑
- 止损线会跟随当前持仓状态动态变化
- 止损后冷却一段时间再重入
那就说明这套逻辑已经超出 IndicatorStrategy 该承担的范围,应该迁移到 ScriptStrategy。
下面这个例子展示了一个更符合开发者思维的完整结构:元数据、默认配置、指标计算、信号生成、图表输出分层清楚。
my_indicator_name = "EMA Pullback Strategy"
my_indicator_description = "Buy pullbacks above the slow EMA and exit on trend failure."
# @param fast_len int 20 Fast EMA length
# @param slow_len int 50 Slow EMA length
# @param rsi_len int 14 RSI length
# @param rsi_floor float 50 Minimum RSI for entry
# @strategy stopLossPct 0.03
# @strategy takeProfitPct 0.06
# @strategy entryPct 0.25
# @strategy tradeDirection long
df = df.copy()
fast_len = int(params.get('fast_len', 20))
slow_len = int(params.get('slow_len', 50))
rsi_len = int(params.get('rsi_len', 14))
rsi_floor = float(params.get('rsi_floor', 50.0))
ema_fast = df['close'].ewm(span=fast_len, adjust=False).mean()
ema_slow = df['close'].ewm(span=slow_len, adjust=False).mean()
delta = df['close'].diff()
gain = delta.clip(lower=0).ewm(alpha=1 / rsi_len, adjust=False).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1 / rsi_len, adjust=False).mean()
rs = gain / loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
trend_up = ema_fast > ema_slow
pullback_done = df['close'] > ema_fast
rsi_ok = rsi > rsi_floor
raw_buy = trend_up & pullback_done & rsi_ok & (~trend_up.shift(1).fillna(False))
raw_sell = (ema_fast < ema_slow) | (rsi < 45)
buy = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
sell = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
df['buy'] = buy
df['sell'] = sell
buy_marks = [df['low'].iloc[i] * 0.995 if buy.iloc[i] else None for i in range(len(df))]
sell_marks = [df['high'].iloc[i] * 1.005 if sell.iloc[i] else None for i in range(len(df))]
output = {
"name": my_indicator_name,
"plots": [
{
"name": "EMA Fast",
"data": ema_fast.fillna(0).tolist(),
"color": "#1890ff",
"overlay": True
},
{
"name": "EMA Slow",
"data": ema_slow.fillna(0).tolist(),
"color": "#faad14",
"overlay": True
},
{
"name": "RSI",
"data": rsi.fillna(0).tolist(),
"color": "#722ed1",
"overlay": False
}
],
"signals": [
{
"type": "buy",
"text": "B",
"data": buy_marks,
"color": "#00E676"
},
{
"type": "sell",
"text": "S",
"data": sell_marks,
"color": "#FF5252"
}
]
}这个例子刻意强调了几件事:
- 先算指标,再出信号
- 进出场逻辑通过布尔列表达
- 固定风险默认值通过
# @strategy单独声明 - 图表输出是最后一步,不要和信号逻辑搅在一起
下面这个版本更接近 QuantDinger 当前真实使用方式:
- 用
# @param暴露常调参数 - 用
# @strategy暴露默认止损、止盈、仓位和跟踪止损 - 显式声明
tradeDirection,让代码、保存后的策略、回测面板保持一致 - 杠杆仍然留给产品 UI 管,不写进源码
my_indicator_name = "Breakout Retest With Direction Control"
my_indicator_description = "Breakout-and-retest logic with platform-friendly params and default risk settings."
# @param breakout_len int 20 Breakout lookback bars
# @param retest_buffer float 0.002 Retest tolerance ratio
# @param volume_mult float 1.2 Minimum volume filter
# @param ema_filter_len int 50 Trend filter EMA length
# @strategy stopLossPct 0.02
# @strategy takeProfitPct 0.05
# @strategy entryPct 0.2
# @strategy trailingEnabled true
# @strategy trailingStopPct 0.015
# @strategy trailingActivationPct 0.03
# @strategy tradeDirection both
df = df.copy()
breakout_len = int(params.get('breakout_len', 20))
retest_buffer = float(params.get('retest_buffer', 0.002))
volume_mult = float(params.get('volume_mult', 1.2))
ema_filter_len = int(params.get('ema_filter_len', 50))
ema_filter = df['close'].ewm(span=ema_filter_len, adjust=False).mean()
range_high = df['high'].rolling(breakout_len).max().shift(1)
range_low = df['low'].rolling(breakout_len).min().shift(1)
volume_avg = df['volume'].rolling(breakout_len).mean()
long_breakout = df['close'] > range_high
long_retest_ok = df['low'] <= range_high * (1 + retest_buffer)
long_volume_ok = df['volume'] >= volume_avg * volume_mult
long_trend_ok = df['close'] > ema_filter
short_breakout = df['close'] < range_low
short_retest_ok = df['high'] >= range_low * (1 - retest_buffer)
short_volume_ok = df['volume'] >= volume_avg * volume_mult
short_trend_ok = df['close'] < ema_filter
raw_buy = long_breakout & long_retest_ok & long_volume_ok & long_trend_ok
raw_sell = short_breakout & short_retest_ok & short_volume_ok & short_trend_ok
buy = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
sell = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
df['buy'] = buy
df['sell'] = sell
buy_marks = [df['low'].iloc[i] * 0.995 if buy.iloc[i] else None for i in range(len(df))]
sell_marks = [df['high'].iloc[i] * 1.005 if sell.iloc[i] else None for i in range(len(df))]
output = {
"name": my_indicator_name,
"plots": [
{
"name": "EMA Filter",
"data": ema_filter.fillna(0).tolist(),
"color": "#1890ff",
"overlay": True
},
{
"name": "Range High",
"data": range_high.fillna(0).tolist(),
"color": "#52c41a",
"overlay": True
},
{
"name": "Range Low",
"data": range_low.fillna(0).tolist(),
"color": "#f5222d",
"overlay": True
}
],
"signals": [
{
"type": "buy",
"text": "L",
"data": buy_marks,
"color": "#00E676"
},
{
"type": "sell",
"text": "S",
"data": sell_marks,
"color": "#FF5252"
}
]
}这个例子为什么更贴近平台:
# @param的值可以直接被 AI 调参或手动参数修改流程接管# @strategy能和保存后的策略默认值、右侧回测面板风险配置更自然地对齐tradeDirection both让人一眼看出这份代码本身就是为多空双向设计的- 杠杆继续交给产品配置层,不会被藏进源码里造成误解
当策略需要“运行时状态”而不是“纯 dataframe 信号”时,就该迁移到 ScriptStrategy。
典型信号包括:
- 止损止盈依赖当前持仓,而不是仅依赖历史序列
- 开仓后要动态移动止损
- 需要部分平仓或加仓
- 首次开仓和再次开仓逻辑不同
- 需要冷却期、节流、bot 风格执行规则
面向当前产品链路,最稳妥的约定是:
def on_init(ctx): ...def on_bar(ctx, bar): ...
原因是:
- 运行时编译器真正强制的是
on_bar - 但部分产品侧校验路径仍然要求源码里同时存在
on_init和on_bar - 为了避免“运行时能跑、校验却不过”的不一致,建议两个函数都写,即使
on_init只是做初始化或打印日志
bar 通常提供:
bar.openbar.highbar.lowbar.closebar.volumebar.timestamp
ctx 当前通常提供:
ctx.param(name, default=None)ctx.bars(n=1)ctx.positionctx.balancectx.equityctx.log(message)ctx.buy(price=None, amount=None)ctx.sell(price=None, amount=None)ctx.close_position()
补充说明:
ctx不会直接把完整交易配置对象暴露给脚本- 杠杆、交易标的、交易场所、账户凭证等,应放在产品配置层,而不是写死在脚本里
- 脚本源码内部需要的默认参数,优先通过
ctx.param(...)管理
ctx.position 同时支持数值判断和字段访问,例如:
if not ctx.position:
...
if ctx.position > 0:
...
if ctx.position["side"] == "long":
...def on_init(ctx):
ctx.log("strategy initialized")
def on_bar(ctx, bar):
stop_loss_pct = ctx.param("stop_loss_pct", 0.03)
take_profit_pct = ctx.param("take_profit_pct", 0.06)
order_amount = ctx.param("order_amount", 1)
bars = ctx.bars(30)
if len(bars) < 20:
return
closes = [b.close for b in bars]
ma_fast = sum(closes[-10:]) / 10
ma_slow = sum(closes[-20:]) / 20
if not ctx.position and ma_fast > ma_slow:
ctx.buy(price=bar.close, amount=order_amount)
return
if not ctx.position:
return
if ctx.position["side"] != "long":
return
entry_price = ctx.position["entry_price"]
if bar.close <= entry_price * (1 - stop_loss_pct):
ctx.close_position()
return
if bar.close >= entry_price * (1 + take_profit_pct):
ctx.close_position()
return
if ma_fast < ma_slow:
ctx.close_position()这种写法适合“止盈止损属于运行时持仓管理”的场景,而不是单纯图表信号输出。
仓位语义还要补一句:
- 在当前系统里,保存后的策略回测,仓位大小仍然主要由规范化后的交易配置决定,例如
entryPct - 因此
ctx.buy()/ctx.sell()里的amount更适合理解成运行时下单意图,而不是回测仓位的唯一来源 - 真正准备上模拟盘或实盘前,一定要先通过“保存后的策略回测”核对实际仓位暴露
大多数 ScriptStrategy 都运行在已收盘 K 线语义下:
- 引擎会在 bar 确认收盘后调用
on_bar(ctx, bar) - 这也是普通策略回测和逐 bar 实盘最接近的心智模型
当前系统里还存在 bot 风格运行模式:
- bot 模式下,系统可能会基于最新价格构造“类 tick 的伪 bar”反复调用
on_bar - 这种模式更适合网格、DCA 或其他更偏机器人执行的策略
- 如果你的脚本是为 bot 模式设计的,应该和标准 bar-close 策略分开测试,不要混为一谈
下面这个例子更接近平台里真实可落地的实盘脚本写法:
- 用
ctx.param(...)管脚本级默认参数 - 先看
ctx.position,再决定是开仓、反手、减仓还是全部平仓 - 用
ctx.buy()/ctx.sell()表达方向性下单意图 - 当你的语义是“现在全部退出”时,用
ctx.close_position()最明确
def on_init(ctx):
ctx.log("live strategy initialized")
def on_bar(ctx, bar):
fast_len = int(ctx.param("fast_len", 10))
slow_len = int(ctx.param("slow_len", 30))
risk_pct = float(ctx.param("risk_pct", 0.25))
stop_loss_pct = float(ctx.param("stop_loss_pct", 0.02))
take_profit_pct = float(ctx.param("take_profit_pct", 0.05))
allow_short = bool(ctx.param("allow_short", True))
bars = ctx.bars(slow_len + 5)
if len(bars) < slow_len:
return
closes = [b.close for b in bars]
fast_ma = sum(closes[-fast_len:]) / fast_len
slow_ma = sum(closes[-slow_len:]) / slow_len
price = bar.close
if not ctx.position:
if fast_ma > slow_ma:
ctx.buy(price=price, amount=risk_pct)
return
if allow_short and fast_ma < slow_ma:
ctx.sell(price=price, amount=risk_pct)
return
return
if ctx.position["side"] == "long":
entry_price = float(ctx.position["entry_price"])
if price <= entry_price * (1 - stop_loss_pct):
ctx.close_position()
return
if price >= entry_price * (1 + take_profit_pct):
ctx.close_position()
return
if allow_short and fast_ma < slow_ma:
ctx.sell(price=price, amount=risk_pct)
return
if ctx.position["side"] == "short":
entry_price = float(ctx.position["entry_price"])
if price >= entry_price * (1 + stop_loss_pct):
ctx.close_position()
return
if price <= entry_price * (1 - take_profit_pct):
ctx.close_position()
return
if fast_ma > slow_ma:
ctx.buy(price=price, amount=risk_pct)
return这个例子重点演示了:
ctx.param(...)让脚本默认值集中且清晰ctx.position决定当前是空仓、做多还是做空分支ctx.buy()/ctx.sell()表达的是方向性意图,不只是孤立的“开多”或“开空”- 当规则的语义是“现在全部退出”时,
ctx.close_position()最不容易产生歧义
下面这些回测 / 实盘差异一定要记住:
- 标准脚本回测和普通实盘模式都以“已确认收盘的 bar”为核心,但 bot 模式可能会用类 tick 的伪 bar 反复驱动脚本
amount更适合理解成运行时下单意图;保存后的策略回测,仓位大小仍然主要受entryPct这类规范化交易配置影响- 当你在多头持仓中调用
ctx.sell(),或在空头持仓中调用ctx.buy()时,实际效果可能会根据运行时状态与产品配置表现为“先平后反手”一类意图 - 如果你要的是明确的“全部平仓”,优先用
ctx.close_position(),不要依赖隐式解释
保存后的策略会被后端解析成统一快照,再进入回测或执行链路。常见字段包括:
strategy_typestrategy_modestrategy_codeindicator_configtrading_config
当前常见 run_type 包括:
indicatorstrategy_indicatorstrategy_script
当前限制包括:
cross_sectional在当前策略快照链路中不支持ScriptStrategy当前不支持cross_sectional实盘运行- 脚本策略回测当前不会走指标侧的 MTF 执行路径
- 策略回测要求 symbol 合法且代码非空
- 只使用已完成 bar 的信息
- 优先使用
shift(1)做确认 - 不要在信号逻辑中使用
shift(-1)
滚动窗口和 EWM 都会产生前导 NaN,生成信号前必须先清理。
所有 plot['data'] 和 signal['data'] 都必须与 len(df) 完全一致。
核心指标计算优先用 pandas 原生向量化逻辑,不要把主逻辑写成逐行循环。
ScriptStrategy 尽量避免 ctx 外部的隐式状态、随机行为,以及含糊不清的下单意图。
- 指标型默认值用
# @param和# @strategy - 脚本型默认值优先用
ctx.param() - 杠杆、成交时机、交易所和账户凭证放在产品配置层,不要硬编码
说明数据库结构版本落后于当前代码,需要对 qd_strategies_trading 执行对应迁移。
说明 ScriptStrategy 缺少必需的 on_bar。
说明当前 UI 校验器要求源码里同时存在这两个函数。
说明保存后的策略在当前模式下没有有效代码。
说明某个 plot 或 signal 的数组长度没有和 df 对齐。
优先检查这几件事:
- 有没有误用未来数据
buy/sell是否做成了边缘触发- 是否同时混用了“信号退出”和“引擎退出”却没有说明清楚
# @strategy默认值是否真的符合策略风格
如果策略创建、校验、回测或执行失败,请优先查后端日志。常见问题包括:
- 数据库结构不匹配
- JSON / 配置载荷格式错误
- 代码校验失败
- 市场 / symbol 不匹配
- 交易所凭证或配置异常
这是目前最符合产品链路的实战流程。
在策略还处于构思阶段时,优先从 Indicator IDE 开始:
- 先把指标逻辑写在
df上。 - 用
# @param声明可调参数。 - 用
# @strategy声明默认止损、止盈、仓位和方向。 - 补齐图表展示需要的
plots和signals。 - 先跑指标侧回测,确认信号密度、图表观感和成交语义都合理。
这个阶段的目标不是立刻上实盘,而是先把策略逻辑变得可见、可测、可迭代。
当指标逻辑已经基本正确后:
- 先跑代码质量检查,排除缺少元数据或明显可疑写法。
- 用真实的标的、周期、手续费、滑点、杠杆配置去跑回测。
- 必要时使用 AI 调参或结构化扫参比较参数组合。
- 把最终采用的参数重新写回源码,让代码本身仍然是最直观的“单一真相”。
这里推荐的分工方式是:
- 信号逻辑由代码表达
# @param和# @strategy表达默认参数和风控- 市场、杠杆、日期区间、执行环境由产品面板管理
当信号模型稳定后:
- 先保存当前指标代码。
- 通过产品流程创建或保存策略记录。
- 确认保存后的策略快照模式、默认值和交易配置都符合预期。
- 从持久化后的策略记录发起策略回测,而不是只看编辑器里的即时结果。
这一步很关键,因为:
- 保存后的策略回测更接近真实执行链路
- 规范化快照可能会补齐成交时机和交易配置默认值
- 很多 symbol、模式、配置、持久化层面的错配,都是在这一步才暴露出来
如果满足下面这些条件,就继续保留 IndicatorStrategy:
- 进出场核心仍然主要靠信号驱动
- 固定止损、止盈、跟踪止损默认值已经足够
- 不需要依赖当前持仓状态做复杂运行时逻辑
如果出现下面这些需求,就应升级成 ScriptStrategy:
- 开仓后要持续盯着持仓逐根处理
- 退出逻辑依赖当前持仓状态,而不只是历史序列
- 需要冷却期、分批止盈、加仓、减仓、机器人式执行逻辑
在真正开启实盘前,至少确认:
- 交易所 / 经纪商 / 标的 / 凭证配置正确。
- 对成交时机的假设已经再次核实。
- 杠杆、方向、仓位大小放在了正确的产品配置层。
- 先用保守仓位、小范围标的做验证。
- 观察运行日志和真实下单行为,再决定是否放大规模。
实盘不是编辑器实验的自然延伸,而是一个单独的验证阶段。
这一节专门列出最容易把回测做“虚高”、把策略行为写混乱、或者把产品配置和策略代码搞串的高频坑点。
错误写法:
# @param fast_len int 20 Fast EMA length
df = df.copy()
fast_len = 20
ema_fast = df['close'].ewm(span=fast_len, adjust=False).mean()正确写法:
# @param fast_len int 20 Fast EMA length
df = df.copy()
fast_len = int(params.get('fast_len', 20))
ema_fast = df['close'].ewm(span=fast_len, adjust=False).mean()为什么:
- 只声明不读取,会让参数变成“看起来能调,实际上调不动”
- 平台代码质量检查也可能对此给出提醒
错误写法:
df['buy'] = df['close'] > ema_fast
df['sell'] = df['close'] < ema_fast正确写法:
raw_buy = df['close'] > ema_fast
raw_sell = df['close'] < ema_fast
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
df['sell'] = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)为什么:
- 如果每根 bar 都重复发信号,进出场、标记、回测解释都会变得混乱
- 大多数策略真正想表达的是“条件刚刚成立时触发一次”,而不是“条件持续成立就一直触发”
错误写法:
# @strategy leverage 10正确写法:
# @strategy entryPct 0.2
# @strategy stopLossPct 0.02
# @strategy takeProfitPct 0.05然后把杠杆放到产品面板或保存后的策略交易配置里。
为什么:
- 杠杆属于执行配置,不属于指标元数据
- 把杠杆藏进代码里,会让回测解释更混乱,也更容易和产品侧配置打架
错误写法:
df['buy'] = (df['close'].shift(-1) > ema_fast).fillna(False)正确写法:
raw_buy = df['close'] > ema_fast
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)为什么:
shift(-1)本质上是在偷看未来数据- 这类策略在回测里往往会“好得离谱”,但一到真实执行就失真
错误心智模型:
ctx.buy(price=bar.close, amount=1.0)“这就等于回测里永远按 100% 仓位开仓。”
正确心智模型:
position_pct = float(ctx.param("risk_pct", 0.25))
ctx.buy(price=bar.close, amount=position_pct)然后再用保存后的策略回测去核对规范化交易配置。
为什么:
- 在当前系统里,保存后的策略回测,仓位大小仍然主要由
entryPct这类规范化配置决定 amount更适合理解成运行时下单意图,而不是历史回测仓位的唯一真相
容易歧义的写法:
if stop_hit:
ctx.sell(price=bar.close, amount=0.25)更清晰的写法:
if stop_hit:
ctx.close_position()为什么:
ctx.buy()/ctx.sell()表达的是方向性意图,最终效果会结合当前持仓状态解释- 如果你的规则语义就是“现在全部退出”,
ctx.close_position()最不容易被误解
容易出问题的写法:
# @strategy stopLossPct 0.02
# @strategy takeProfitPct 0.05
df['sell'] = some_other_exit_condition更好的写法:
# Primary exit: reverse signal
# Secondary protection: engine-managed fixed stop-loss / take-profit
# @strategy stopLossPct 0.02
# @strategy takeProfitPct 0.05
df['sell'] = reverse_signal为什么:
- 两种退出方式并存本身不一定错
- 真正的问题是没人说得清“到底谁是主退出来源”
这一节可以当成“当前平台到底支持什么”的速查页,写策略时可以直接对着查。
| Key | 含义 | 常见写法 | 说明 |
|---|---|---|---|
stopLossPct |
默认止损比例 | # @strategy stopLossPct 0.02 |
引擎读取的默认风控配置 |
takeProfitPct |
默认止盈比例 | # @strategy takeProfitPct 0.05 |
底层解析器允许范围比示例更宽松 |
entryPct |
默认开仓资金占比 | # @strategy entryPct 0.25 |
是回测仓位的重要来源之一 |
trailingEnabled |
是否开启跟踪止损 | # @strategy trailingEnabled true |
布尔值 |
trailingStopPct |
跟踪止损比例 | # @strategy trailingStopPct 0.015 |
通常与 trailing 一起使用 |
trailingActivationPct |
启动跟踪止损前的盈利阈值 | # @strategy trailingActivationPct 0.03 |
通常与 trailing 一起使用 |
tradeDirection |
方向限制 | # @strategy tradeDirection both |
可选 long、short、both |
要点:
- 这些 key 用于指标侧默认策略配置
- 不要把
leverage写进# @strategy - 交易所、标的、凭证、杠杆都应该放在产品配置层
| 部分 | 示例 | 含义 |
|---|---|---|
| 名称 | fast_len |
参数键名 |
| 类型 | int / float / bool / str / string |
支持的类型 |
| 默认值 | 20 |
系统看到的默认值 |
| 描述 | Fast EMA length |
给人看的说明 |
示例:
# @param fast_len int 20 Fast EMA length
# @param allow_short bool true Allow short entries随后用下面这种方式读取:
fast_len = int(params.get('fast_len', 20))
allow_short = bool(params.get('allow_short', True))| 项目 | 类型 | 含义 |
|---|---|---|
ctx.param(name, default) |
方法 | 读取或初始化脚本级默认参数 |
ctx.bars(n=1) |
方法 | 取得当前运行时之前的最近若干根 bar |
ctx.log(message) |
方法 | 写策略日志 |
ctx.buy(price=None, amount=None) |
方法 | 表达买入 / 做多方向意图 |
ctx.sell(price=None, amount=None) |
方法 | 表达卖出 / 做空方向意图 |
ctx.close_position() |
方法 | 显式全部平仓 |
ctx.position |
字段 | 当前持仓对象 |
ctx.balance |
字段 | 当前余额快照 |
ctx.equity |
字段 | 当前权益快照 |
ctx.position 常见字段:
| 字段 | 含义 |
|---|---|
side |
long、short,或空字符串表示空仓 |
size |
当前持仓大小 |
entry_price |
平均开仓价 |
direction |
1、-1、0 |
amount |
运行时数量镜像 |
| 字段 | 含义 |
|---|---|
bar.open |
开盘价 |
bar.high |
最高价 |
bar.low |
最低价 |
bar.close |
收盘价 |
bar.volume |
成交量 |
bar.timestamp |
当前运行时传入的时间值 |
顶层结构通常写成:
output = {
"name": my_indicator_name,
"plots": [],
"signals": [],
"calculatedVars": {}
}顶层常见 key:
| Key | 是否必需 | 含义 |
|---|---|---|
name |
建议提供 | 展示名称 |
plots |
建议提供 | 图表曲线输出 |
signals |
建议提供 | 买卖点标记输出 |
calculatedVars |
可选 | 额外元数据或计算结果 |
每个 plot 项常见字段:
| Key | 含义 |
|---|---|
name |
曲线名称 |
data |
与 len(df) 对齐的数组 |
color |
显示颜色 |
overlay |
是否叠加在主图上 |
type |
可选的渲染提示 |
每个 signal 项常见字段:
| Key | 含义 |
|---|---|
type |
buy 或 sell |
text |
标记文本 |
color |
标记颜色 |
data |
与 len(df) 对齐的数组;无信号位置用 None |
df['buy']和df['sell']应该是布尔值,并与df长度完全对齐- 尽量使用边缘触发信号
- 不要在信号逻辑里使用
shift(-1) - 当规则语义明显是“全部退出”时,优先用
ctx.close_position() amount更适合作为运行时下单意图,最终仓位仍应通过保存后的策略回测核实
- 先用
IndicatorStrategy把想法原型化。 - 先验证图表、信号密度和 next-bar-open 的回测语义。
- 把
# @param和# @strategy元数据补完整。 - 明确写清楚:退出到底是“信号负责”还是“引擎负责”。
- 保存策略后,再从持久化记录跑策略回测。
- 只有在确实需要运行时仓位管理时,再迁移到
ScriptStrategy。 - 确认配置、凭证和市场语义都正确后,再进入模拟盘或实盘。