Files
quantdingerdocs/STRATEGY_DEV_GUIDE_CN.md
T
2026-07-11 20:14:35 +00:00

1437 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QuantDinger Python v3 策略开发指南
> **平台级契约(必读)**:[信号与执行标准 v1](./SIGNAL_EXECUTION_STANDARD_CN.md) — 适用于所有指标策略的回测/实盘对齐、两路/四路选型、退出负责人与上线清单。本指南侧重教程与示例。
这份指南不是单纯罗列接口,而是站在**策略开发者**视角,回答一个更实际的问题:
**到底应该怎么写一个结构清晰、能回测、能落地成平台策略的指标策略?**
QuantDinger 当前支持两条 Python 开发路径:
- **IndicatorStrategy**:基于 `df` 的指标/信号脚本,用于 Indicator IDE、图表渲染和信号型回测。
- **ScriptStrategy**:基于 `on_init / on_bar` 的事件驱动脚本,用于策略运行时、策略回测与实盘执行。
如果你要从零开始开发一个策略,默认建议是:
1. 先用 `IndicatorStrategy` 把信号逻辑跑通。
2. 先验证图表、信号和回测语义。
3. 只有当你需要运行时状态、动态仓位管理或执行控制时,再升级为 `ScriptStrategy`
---
## 1. 先建立正确心智模型
很多开发者会把**信号逻辑**、**止盈止损**、**仓位管理**、**执行逻辑**混在一起写,结果文档看不懂、代码也不好维护。
### 1.1 IndicatorStrategy 是什么
可以把 `IndicatorStrategy` 理解成:
- 基于 `df` 计算指标序列
- 生成布尔型 `buy` / `sell` 信号
- 通过元数据声明默认策略配置
- 返回 `output` 供图表展示
它最适合:
- 指标研究
- 策略原型验证
- 参数调优
- 信号型回测
- 先做信号、后保存成平台策略的工作流
### 1.2 ScriptStrategy 是什么
可以把 `ScriptStrategy` 理解成:
- 按 bar 逐根执行的运行时逻辑
- 通过 `ctx.position` 读取当前持仓状态
-`ctx.buy()``ctx.sell()``ctx.close_position()` 发出动作
- 把退出、仓位、执行节奏写进代码
它最适合:
- 有状态的执行逻辑
- 动态止盈止损
- 分批加仓、减仓、部分止盈
- 冷却期、重入限制、bot 型执行策略
### 1.3 最重要的分层
对于 `IndicatorStrategy`,请强制把逻辑拆成三层:
1. **指标层**:均线、RSI、ATR、布林带、过滤条件。
2. **信号层**`df['buy']``df['sell']`
3. **风险默认配置层**`# @strategy stopLossPct ...``takeProfitPct``entryPct` 等。
不要把这三层混成一团。
尤其要明确:
- `buy` / `sell` 负责表达**什么时候进出场**
- `# @strategy` 负责表达**引擎默认如何控风险、如何设仓位**
- 杠杆属于产品配置,不属于指标脚本
---
## 2. 应该选哪种模式?
| 使用场景 | 推荐模式 |
|----------|----------|
| 写指标、叠加图表、画买卖点 | `IndicatorStrategy` |
| 研究 dataframe 上的进出场信号 | `IndicatorStrategy` |
| 只想给策略补固定止损、止盈、仓位默认值 | `IndicatorStrategy` |
| 需要逐根读取持仓状态做判断 | `ScriptStrategy` |
| 止损止盈依赖当前持仓状态动态变化 | `ScriptStrategy` |
| 需要分批开平仓、状态机、bot 风格执行 | `ScriptStrategy` |
一个简单判断方法:
- 如果你的逻辑可以表述成“条件 A 出现就买,条件 B 出现就卖”,先用 `IndicatorStrategy`
- 如果你的逻辑更像“开仓后要持续盯着当前持仓,并根据状态做不同反应”,那就应该用 `ScriptStrategy`
---
## 3. 如何开发一个 IndicatorStrategy
这是大多数新策略最推荐的开发路径。
### 3.1 第一步:先把元数据和默认配置写清楚
脚本开头先定义名称、描述、可调参数、默认策略配置。
```python
my_indicator_name = "Trend Pullback Strategy"
my_indicator_description = "Buy pullbacks in an uptrend and exit on weakness."
# signal_form: two_way
# exit_owner: engine
# flip_mode: R2
# @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` 用来定义用户经常调的参数。
格式如下:
```python
# @param <name> <int|float|bool|str|string> <default> <描述>
```
最佳实践:
- 声明后的参数,应该通过 `params.get(...)` 读取
- `string``str` 等价
- 如果声明了参数,却仍然把值硬编码在正文里,平台内置的代码质量检查会给出提醒
`# @strategy` 用来定义策略默认配置,比如:
- `stopLossPct`:止损比例,例如 `0.03` 表示 **标的价格下跌 3%** 触发(01 小数;`0.001` = 0.1%
- `takeProfitPct`:止盈比例,例如 `0.06` 表示 **标的价格上涨 6%** 触发
- `entryPct`:开仓资金占比(01**`1` = 100%**`0.25` = 25%
- `trailingEnabled`
- `trailingStopPct`
- `trailingActivationPct`
- `tradeDirection``long``short``both`
这里有个非常关键的边界:
- 这些是**引擎读取的默认配置**
- 不是让你再去 dataframe 里造一列 `stop_loss`
- 不要在这里写 `leverage`
- 数值尽量保持合理,并结合回测验证;底层解析器允许的范围会比示例更宽松
### 3.2 第二步:复制 dataframe,再算指标
Indicator 代码运行在沙盒里,`pd``np``params` 字典已预置。
推荐开头固定写:
```python
df = df.copy()
```
通常可用列包括:
- `open`
- `high`
- `low`
- `close`
- `volume`
`time` 列可能存在,但不要假设其类型永远一致。
避免这些写法:
- 网络请求
- 文件读写
- 子进程
- `eval``exec``open``__import__``getattr` / `setattr` 等破坏沙盒边界的模式
- `import operator`(以及通过字符串拼接访问 `__class__` / `__globals__` 等 dunder 的绕过写法)
允许 `import` 的白名单模块(其余会被校验拒绝):
`numpy``pandas``math``json``datetime``time``collections``functools``itertools``statistics``decimal``fractions``copy`
环境已预置 `pd``np``params`,一般无需再 `import pandas` / `import numpy`
### 3.3 第三步:把原始条件变成干净的 `buy` / `sell`
回测引擎读取的是两列**布尔信号**:
- `df['buy']`
- `df['sell']`
它们应满足:
- 与 dataframe 长度完全一致
- `fillna(False)` 后为布尔值
- 除非你明确要连续触发,否则应尽量做成边缘触发
推荐模式:
```python
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 都重复发信号。
#### 3.3.1 `tradeDirection` 与 `buy` / `sell` 在引擎里如何解释
指标保存为策略后,后端会把 `df['buy']` / `df['sell']` 规范成执行信号。请按下面表格理解,**不要**在 `both` 模式下把 `buy` 当成“单独的平空列”:
| `tradeDirection` | `buy=True` | `sell=True` |
|------------------|------------|-------------|
| `long` | 开多 `open_long` | 平多 `close_long` |
| `short` | 平空 `close_short` | 开空 `open_short` |
| `both` | 开多 `open_long`;若当前持空则**先平空再开多** | 开空 `open_short`;若当前持多则**先平多再开空** |
要点:
- `both` 与回测 `BacktestService``_both_mode` 语义一致;实盘不应再拆出独立的 `close_short` / `close_long` 去“辅助”表达平仓。
- 若你把「空侧止盈 / 空侧止损」写进 `df['buy']`,在 `both` 下表达的是**退出空头并可能反手做多**,不是“只平空、保持空仓”。
- 若只想平仓、不想反手,请改用 `tradeDirection long/short`、四路布尔列(`open_long` / `close_long` / …),或迁移到 `ScriptStrategy``ctx.close_position()`
常见组合写法(双向肯特纳类策略):
```python
df['buy'] = sig_buy_entry | sig_short_tp | sig_short_sl
df['sell'] = sig_sell_entry | sig_long_tp | sig_long_sl
```
写代码时可以这样自检:回测成交里若出现「平空后立刻开多」,通常就是 `both``buy=True` 的预期行为,而不是引擎 bug。
### 3.4 第四步:先决定“谁负责退出”
止盈止损和仓位管理最容易在这里写乱。
`IndicatorStrategy` 里,退出逻辑通常有两种合法写法,并且必须通过头部契约声明清楚。
#### 写法 A:信号自己负责退出
也就是由你的指标逻辑直接生成 `df['sell']`
典型例子:
- 均线死叉
- RSI 跌破阈值
- 收盘价跌破 ATR 止损线
- 均值回归到目标位后离场
如果退出本身就是策略思想的一部分,用这种写法最自然。
这种写法请声明:
```python
# exit_owner: indicator
# @strategy trailingEnabled false
```
当前后端会把 `exit_owner: indicator` 解释为:服务端固定止损、固定止盈、追踪止损都不主动平仓;退出以指标信号为准。`entryPct` / `tradeDirection` 仍然可以继续作为默认配置使用。
#### 写法 B:引擎负责固定止盈止损
也就是你只定义默认配置,由引擎按固定规则处理:
- `stopLossPct`
- `takeProfitPct`
- `entryPct`
- trailing 系列参数
如果你的信号逻辑想保持简洁,而保护性规则是固定的,就用这种写法。
这种写法请声明:
```python
# exit_owner: engine
```
`exit_owner: engine` 表示服务端价格风控有效。代码里可以保留“趋势反转平仓”这类结构性 `close_*`,但不要再把窄止盈/窄止损触及条件也写进指标信号里。
#### 最佳实践
尽量明确一个“主退出来源”。
例如:
- 如果你的核心逻辑是“金叉进,死叉出”,且没有额外固定价格风控,那退出就主要由指标信号负责,写 `# exit_owner: indicator`
- 如果你的逻辑是“信号进场,固定 3% 止损 + 6% 止盈管理交易”,或只把 `close_*` 当作趋势反转时的结构性平仓,那价格退出主要由引擎负责,写 `# exit_owner: engine`
不要写 `exit_owner: layered`。当前平台未实现第三种退出负责人;如果你确实要混合方案,先按 `engine` 写并在评审里说明哪些 `close_*` 只是趋势反转,不是窄 tp/sl。
### 3.5 第五步:最后再组装 `output`
脚本最后必须赋值 `output`
```python
output = {
"name": "My Strategy",
"plots": [],
"signals": []
}
```
主要支持键:
- `name`
- `plots`
- `signals`
- `calculatedVars`:可选元数据
每个 `plot` 项通常包含:
- `name`
- `data`,长度必须等于 `len(df)`
- `color`
- `overlay`
- 可选 `type`
每个 `signal` 项通常包含:
- `type``buy``sell`
- `text`
- `color`
- `data`:无信号的 bar 用 `None`
### 3.6 第六步:校验回测语义
指标回测是典型的信号驱动:
- 引擎读取 `df['buy']``df['sell']`
- 信号按 bar close 确认
- 通常在**下一根 bar 开盘价**成交
这件事非常重要,因为:
- 你在当前 K 线上画出来的“止损线”不等于系统一定按这根 K 线内部价格成交
- 一旦用了 `shift(-1)`,就基本等于引入未来函数
还要注意一个实现细节:
- 标准工作流下,最常见的成交语义仍然是“收盘确认、下一根开盘成交”
- 但保存后的策略快照,会根据产品配置被规范成 `next_bar_open``same_bar_close`
- 如果你改了成交时机配置,不要凭印象判断结果,要重新回测并核对成交明细
#### 3.6.1 实盘与回测为什么会在“开平仓时间”上差很多
| 维度 | 指标回测 | 指标实盘(默认) |
|------|----------|------------------|
| K 线 | 历史**已收盘** OHLC | 会用最新价**刷新未收盘 K** 的 `high` / `low` / `close` |
| 盘中触及 | 只在整根 K 收盘后才知道 high/low | 未收盘 K 上也可能提前触发「触及中轨 / 外轨」类条件 |
| 信号检查 | 按回测时间轴逐根推进 | 每个 tick 可能重算指标;`exit_signal_mode=immediate` 时平仓类信号可立即下单 |
| 每 tick 下单 | 按回测队列顺序 | 指标模式通常**每 tick 最多执行 1 个**信号(优先平仓) |
若策略逻辑依赖 `high >= 某线``low <= 某线` 这类**盘中触及**,回测往往比实盘**更晚**才出现信号;若未收盘 K 被实时刷新,实盘可能**更早**止盈/止损。
建议(与回测对齐时):
- 策略配置里将 `signal_mode``exit_signal_mode` 设为 **`confirmed`**(只在上一根已收盘 K 上读信号)。
- 指标内已用 `sig_*_tp` / `sig_*_sl` 表达退出时,声明 `# exit_owner: indicator` 并保持 `# @strategy trailingEnabled false`,否则会出现「指标退出 + 服务端追踪止损」**双重平仓**,并放大时间差。
- 上线前对比:回测成交时间、实盘日志里的 `Signal submitted` / `server_trailing_stop` 是否成对出现。
实盘平仓若短暂出现「数量为 0」:执行器会**再次同步交易所持仓并重新解析数量**;若交易所确已空仓,才会拒单(例如移动止损已先平掉)。
---
## 4. 止盈止损和仓位管理到底怎么写
这一节就是给开发者的直接答案。
### 4.1 在 IndicatorStrategy 里写固定止损、止盈、仓位
如果你想要的是固定默认配置,就写成 `# @strategy`
```python
# @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 和引擎都能直接识别这些默认值
### 4.2 在 IndicatorStrategy 里写“指标驱动型止损”
如果你的“止损”本质上是策略逻辑的一部分,那就不要假装成外部配置,而是直接写进 `sell` 信号。
例如:跌破 ATR 风格止损线就卖出。
```python
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)
```
这种写法表示:
- 退出属于你的指标逻辑
- 引擎不是替你“发明”一个止损
- 你最好在描述或注释里说明这一点
### 4.3 IndicatorStrategy 里的仓位管理边界
`IndicatorStrategy` 来说,仓位管理应该尽量保持简单:
-`entryPct` 管默认开仓资金占比
-`tradeDirection` 管做多 / 做空 / 双向
- 用固定的止损止盈或 trailing 默认值做保护
如果你需要下面这些能力:
- 分批加仓、减仓
- 部分止盈
- 开仓前后用不同逻辑
- 止损线会跟随当前持仓状态动态变化
- 止损后冷却一段时间再重入
那就说明这套逻辑已经超出 `IndicatorStrategy` 该承担的范围,应该迁移到 `ScriptStrategy`
---
## 5. 完整的 IndicatorStrategy 示例
下面这个例子展示了一个更符合开发者思维的完整结构:元数据、默认配置、指标计算、信号生成、图表输出分层清楚。
```python
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` 单独声明
- 图表输出是最后一步,不要和信号逻辑搅在一起
### 5.1 一个更贴近平台 UI 的示例
下面这个版本更接近 QuantDinger 当前真实使用方式:
-`# @param` 暴露常调参数
-`# @strategy` 暴露默认止损、止盈、仓位和跟踪止损
- 显式声明 `tradeDirection`,让代码、保存后的策略、回测面板保持一致
- 杠杆仍然留给产品 UI 管,不写进源码
```python
my_indicator_name = "Breakout Retest With Direction Control"
my_indicator_description = "Breakout-and-retest logic with platform-friendly params and default risk settings."
# signal_form: two_way
# exit_owner: engine
# flip_mode: R2
# @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` 让人一眼看出这份代码本身就是为多空双向设计的
- 杠杆继续交给产品配置层,不会被藏进源码里造成误解
---
## 6. 什么时候该切到 ScriptStrategy
当策略需要“运行时状态”而不是“纯 dataframe 信号”时,就该迁移到 `ScriptStrategy`
典型信号包括:
- 止损止盈依赖当前持仓,而不是仅依赖历史序列
- 开仓后要动态移动止损
- 需要部分平仓或加仓
- 首次开仓和再次开仓逻辑不同
- 需要冷却期、节流、bot 风格执行规则
### 6.1 必需函数
面向当前产品链路,最稳妥的约定是:
- `def on_init(ctx): ...`
- `def on_bar(ctx, bar): ...`
原因是:
- 运行时编译器真正强制的是 `on_bar`
- 但部分产品侧校验路径仍然要求源码里同时存在 `on_init``on_bar`
- 为了避免“运行时能跑、校验却不过”的不一致,建议两个函数都写,即使 `on_init` 只是做初始化或打印日志
### 6.2 可用对象
`bar` 通常提供:
- `bar.open`
- `bar.high`
- `bar.low`
- `bar.close`
- `bar.volume`
- `bar.timestamp`
`ctx` 当前通常提供:
- `ctx.param(name, default=None)`
- `ctx.bars(n=1)`
- `ctx.position`
- `ctx.balance`
- `ctx.equity`
- `ctx.log(message)`
- `ctx.buy(price=None, amount=None)`
- `ctx.sell(price=None, amount=None)`
- `ctx.close_position()`
补充说明:
- `ctx` 不会直接把完整交易配置对象暴露给脚本
- 杠杆、交易标的、交易场所、账户凭证等,应放在产品配置层,而不是写死在脚本里
- 脚本源码内部需要的默认参数,优先通过 `ctx.param(...)` 管理
`ctx.position` 同时支持数值判断和字段访问,例如:
```python
if not ctx.position:
...
if ctx.position > 0:
...
if ctx.position["side"] == "long":
...
```
### 6.3 一个带运行时退出的 ScriptStrategy 示例
```python
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` 更适合理解成运行时下单意图,而不是回测仓位的唯一来源
- 真正准备上模拟盘或实盘前,一定要先通过“保存后的策略回测”核对实际仓位暴露
### 6.4 普通脚本模式与 bot 模式
大多数 `ScriptStrategy` 都运行在**已收盘 K 线**语义下:
- 引擎会在 bar 确认收盘后调用 `on_bar(ctx, bar)`
- 这也是普通策略回测和逐 bar 实盘最接近的心智模型
当前系统里还存在 bot 风格运行模式:
- bot 模式下,系统可能会基于最新价格构造“类 tick 的伪 bar”反复调用 `on_bar`
- 这种模式更适合网格、DCA 或其他更偏机器人执行的策略
- 如果你的脚本是为 bot 模式设计的,应该和标准 bar-close 策略分开测试,不要混为一谈
### 6.5 一个更贴近平台实盘的 ScriptStrategy 示例
下面这个例子更接近平台里真实可落地的实盘脚本写法:
-`ctx.param(...)` 管脚本级默认参数
- 先看 `ctx.position`,再决定是开仓、反手、减仓还是全部平仓
-`ctx.buy()` / `ctx.sell()` 表达方向性下单意图
- 当你的语义是“现在全部退出”时,用 `ctx.close_position()` 最明确
```python
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()`,不要依赖隐式解释
---
## 7. 回测、持久化与当前限制
保存后的策略会被后端解析成统一快照,再进入回测或执行链路。常见字段包括:
- `strategy_type`
- `strategy_mode`
- `strategy_code`
- `indicator_config`
- `trading_config`
当前常见 `run_type` 包括:
- `indicator`
- `strategy_indicator`
- `strategy_script`
当前限制包括:
- `cross_sectional` 在当前策略快照链路中不支持
- `ScriptStrategy` 当前不支持 `cross_sectional` 实盘运行
- 脚本策略回测当前不会走指标侧的 MTF 执行路径
- 策略回测要求 symbol 合法且代码非空
### 7.1 脚本策略回测的成交假设(与指标「严格模式」不同)
脚本策略回测**没有**指标 IDE 里的「严格 / 非严格」开关。UI 上应显示为:
**脚本标准回测 · 逐 bar · 下一根开盘成交**
含义:
1. 按策略周期(如 5m)逐根拉 K 线,每根已收盘 bar 调用一次 `on_bar(ctx, bar)`
2. 脚本在当根 bar 内通过 `ctx.buy()` / `ctx.sell()` / `ctx.close_position()` 表达下单意图。
3. 撮合层默认把成交推到**下一根 K 线开盘价**(并计滑点/手续费),与 `trading_config.execution.signalTiming = next_bar_open` 一致。
4. 仓位大小仍主要受 `entryPct` 等交易配置约束,而不是脚本里 `amount` 的唯一来源。
这与 **IndicatorStrategy** 的严格模式(整表布尔信号 + 可选 MTF 子周期)是两条不同路径,不要混为一谈。
### 7.2 交易机器人 vs 脚本策略 vs 指标策略
| 类型 | 代码存放 | 典型入口 | 说明 |
|------|----------|----------|------|
| **IndicatorStrategy** | `qd_indicator_codes.code` | 指标 IDE | `df` + `buy/sell` 信号,适合研究与信号型回测 |
| **ScriptStrategy** | `qd_strategies_trading.strategy_code` | 策略工作室 `/strategy-script` | `on_init` + `on_bar`,适合有状态逻辑 |
| **Trading Bot** | 同上 `strategy_code``strategy_mode=bot` | 交易机器人向导 | 参数化模板;Live 网格等由专用引擎执行 |
**「克隆为脚本」**(机器人详情页)会把 `strategy_code` 复制为新的 **ScriptStrategy**,跳转到 **策略工作室** 编辑,**不会**写入指标 IDE 的 `indicator_code`
- **网格机器人** 的 `strategy_code` 只是占位脚本(`on_bar: pass`),真实逻辑在 Live 挂单引擎;克隆后编辑器里看起来「几乎为空」是预期行为,不是 bug。
- **马丁 / 趋势 / DCA** 等机器人会生成完整 Python 模板,克隆后应能看到可编辑代码。
- 若克隆后在策略工作室仍看不到代码,请刷新页面;新版本会在编辑时调用 `/api/strategies/detail` 拉取完整 `strategy_code`
---
## 8. 最佳实践
### 8.1 始终避免未来函数
- 只使用已完成 bar 的信息
- 优先使用 `shift(1)` 做确认
- 不要在信号逻辑中使用 `shift(-1)`
### 8.2 显式处理 NaN
滚动窗口和 EWM 都会产生前导 NaN,生成信号前必须先清理。
### 8.3 保持所有序列长度一致
所有 `plot['data']``signal['data']` 都必须与 `len(df)` 完全一致。
### 8.4 IndicatorStrategy 尽量保持向量化
核心指标计算优先用 pandas 原生向量化逻辑,不要把主逻辑写成逐行循环。
### 8.5 ScriptStrategy 保持确定性
`ScriptStrategy` 尽量避免 `ctx` 外部的隐式状态、随机行为,以及含糊不清的下单意图。
### 8.6 把配置放在正确层级
- 指标型默认值用 `# @param``# @strategy`
- 脚本型默认值优先用 `ctx.param()`
- 杠杆、成交时机、交易所和账户凭证放在产品配置层,不要硬编码
---
## 9. 故障排除
### 9.1 `column "strategy_mode" does not exist`
说明数据库结构版本落后于当前代码,需要对 `qd_strategies_trading` 执行对应迁移。
### 9.2 `Strategy script must define on_bar(ctx, bar)`
说明 `ScriptStrategy` 缺少必需的 `on_bar`
### 9.3 `Missing required functions: on_init, on_bar`
说明当前 UI 校验器要求源码里同时存在这两个函数。
### 9.4 `Strategy code is empty and cannot be backtested`
说明保存后的策略在当前模式下没有有效代码。
### 9.5 图表长度不一致
说明某个 plot 或 signal 的数组长度没有和 `df` 对齐。
### 9.6 回测结果很奇怪
优先检查这几件事:
- 有没有误用未来数据
- `buy` / `sell` 是否做成了边缘触发
- 是否同时混用了“信号退出”和“引擎退出”却没有说明清楚
- `# @strategy` 默认值是否真的符合策略风格
### 9.7 后端日志排查
如果策略创建、校验、回测或执行失败,请优先查后端日志。常见问题包括:
- 数据库结构不匹配
- JSON / 配置载荷格式错误
- 代码校验失败
- 市场 / symbol 不匹配
- 交易所凭证或配置异常
---
## 10. 完整工作流:从 Indicator IDE 到保存成策略再到实盘
这是目前最符合产品链路的实战流程。
### 10.1 先在 Indicator IDE 里做原型
在策略还处于构思阶段时,优先从 Indicator IDE 开始:
1. 先把指标逻辑写在 `df` 上。
2.`# @param` 声明可调参数。
3.`# @strategy` 声明默认止损、止盈、仓位和方向。
4. 补齐图表展示需要的 `plots``signals`
5. 先跑指标侧回测,确认信号密度、图表观感和成交语义都合理。
这个阶段的目标不是立刻上实盘,而是先把策略逻辑变得可见、可测、可迭代。
### 10.2 在产品里调参与校验
当指标逻辑已经基本正确后:
1. 先跑代码质量检查,排除缺少元数据或明显可疑写法。
2. 用真实的标的、周期、手续费、滑点、杠杆配置去跑回测。
3. 必要时使用 AI 调参或结构化扫参比较参数组合。
4. 把最终采用的参数重新写回源码,让代码本身仍然是最直观的“单一真相”。
这里推荐的分工方式是:
- 信号逻辑由代码表达
- `# @param``# @strategy` 表达默认参数和风控
- 市场、杠杆、日期区间、执行环境由产品面板管理
### 10.3 把指标保存成策略
当信号模型稳定后:
1. 先保存当前指标代码。
2. 通过产品流程创建或保存策略记录。
3. 确认保存后的策略快照模式、默认值和交易配置都符合预期。
4. 从持久化后的策略记录发起策略回测,而不是只看编辑器里的即时结果。
这一步很关键,因为:
- 保存后的策略回测更接近真实执行链路
- 规范化快照可能会补齐成交时机和交易配置默认值
- 很多 symbol、模式、配置、持久化层面的错配,都是在这一步才暴露出来
### 10.4 判断 IndicatorStrategy 是否已经够用
如果满足下面这些条件,就继续保留 `IndicatorStrategy`
- 进出场核心仍然主要靠信号驱动
- 固定止损、止盈、跟踪止损默认值已经足够
- 不需要依赖当前持仓状态做复杂运行时逻辑
如果出现下面这些需求,就应升级成 `ScriptStrategy`
- 开仓后要持续盯着持仓逐根处理
- 退出逻辑依赖当前持仓状态,而不只是历史序列
- 需要冷却期、分批止盈、加仓、减仓、机器人式执行逻辑
### 10.5 进入模拟盘或实盘前的最后检查
在真正开启实盘前,至少确认:
1. 交易所 / 经纪商 / 标的 / 凭证配置正确。
2. 对成交时机的假设已经再次核实(`signal_mode` / `exit_signal_mode` 是否与回测一致,见 §3.6.1)。
3. `tradeDirection both` 时已理解 `buy` / `sell` 的反手语义(§3.3.1),指标内 tp/sl 与 `trailingEnabled` 未重复(§11.7)。
4. 杠杆、方向、仓位大小放在了正确的产品配置层。
5. 先用保守仓位、小范围标的做验证。
6. 观察运行日志和真实下单行为(含 `server_trailing_stop`、拒单原因),再决定是否放大规模。
实盘不是编辑器实验的自然延伸,而是一个单独的验证阶段。
---
## 11. 常见错误示例 vs 正确写法
这一节专门列出最容易把回测做“虚高”、把策略行为写混乱、或者把产品配置和策略代码搞串的高频坑点。
### 11.1 声明了 `# @param`,却根本没读取
错误写法:
```python
# @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()
```
正确写法:
```python
# @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()
```
为什么:
- 只声明不读取,会让参数变成“看起来能调,实际上调不动”
- 平台代码质量检查也可能对此给出提醒
### 11.2 `buy` / `sell` 每根 bar 都在触发
错误写法:
```python
df['buy'] = df['close'] > ema_fast
df['sell'] = df['close'] < ema_fast
```
正确写法:
```python
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 都重复发信号,进出场、标记、回测解释都会变得混乱
- 大多数策略真正想表达的是“条件刚刚成立时触发一次”,而不是“条件持续成立就一直触发”
### 11.3 把杠杆写进策略源码
错误写法:
```python
# @strategy leverage 10
```
正确写法:
```python
# @strategy entryPct 0.2
# @strategy stopLossPct 0.02
# @strategy takeProfitPct 0.05
```
然后把杠杆放到产品面板或保存后的策略交易配置里。
为什么:
- 杠杆属于执行配置,不属于指标元数据
- 把杠杆藏进代码里,会让回测解释更混乱,也更容易和产品侧配置打架
### 11.4 误用 `shift(-1)`,把未来函数写进策略
错误写法:
```python
df['buy'] = (df['close'].shift(-1) > ema_fast).fillna(False)
```
正确写法:
```python
raw_buy = df['close'] > ema_fast
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
```
为什么:
- `shift(-1)` 本质上是在偷看未来数据
- 这类策略在回测里往往会“好得离谱”,但一到真实执行就失真
### 11.5 在 `ScriptStrategy` 里把 `amount` 当成绝对回测仓位
错误心智模型:
```python
ctx.buy(price=bar.close, amount=1.0)
```
“这就等于回测里永远按 100% 仓位开仓。”
正确心智模型:
```python
position_pct = float(ctx.param("risk_pct", 0.25))
ctx.buy(price=bar.close, amount=position_pct)
```
然后再用保存后的策略回测去核对规范化交易配置。
为什么:
- 在当前系统里,保存后的策略回测,仓位大小仍然主要由 `entryPct` 这类规范化配置决定
- `amount` 更适合理解成运行时下单意图,而不是历史回测仓位的唯一真相
### 11.6 明明想“全部平仓”,却写成了 `ctx.sell()` / `ctx.buy()`
容易歧义的写法:
```python
if stop_hit:
ctx.sell(price=bar.close, amount=0.25)
```
更清晰的写法:
```python
if stop_hit:
ctx.close_position()
```
为什么:
- `ctx.buy()` / `ctx.sell()` 表达的是方向性意图,最终效果会结合当前持仓状态解释
- 如果你的规则语义就是“现在全部退出”,`ctx.close_position()` 最不容易被误解
### 11.7 信号退出和引擎退出混着用,却没写说明
容易出问题的写法:
```python
# @strategy stopLossPct 0.02
# @strategy takeProfitPct 0.05
# @strategy trailingEnabled true
df['sell'] = some_other_exit_condition # 指标内多侧止盈
# 同时 buy 里还有 sig_short_tp / sig_short_sl …
```
更好的写法(二选一,并在注释里写清楚):
```python
# 方案 A:退出全部由指标信号负责(推荐用于中轨/轨道触及类策略)
# exit_owner: indicator
# @strategy trailingEnabled false
# Primary exit: close_* 或 df['buy'] / df['sell'] 内的 tp/sl 条件
# stopLossPct / takeProfitPct / trailing* 不作为服务端退出
# 方案 B:退出由引擎风控负责(固定止损/止盈/移动止损)
# exit_owner: engine
# @strategy trailingEnabled true
# @strategy trailingStopPct 0.0025
# @strategy trailingActivationPct 0.0037
# df['buy'] / df['sell'] 或 open_* 只负责入场;结构性反转 close_* 可以保留,窄 tp/sl 不要再写进指标
```
为什么:
- 两种退出方式并存时,回测与实盘都会执行**先到为准**的那条路径,日志里可能出现 `server_trailing_stop` 与指标 `close_*` 紧挨着,甚至「平仓数量为 0」的拒单(仓位已被移动止损平掉)。
- `exit_owner: indicator` 时,后端会关闭服务端固定止损、固定止盈和追踪止损;`# @strategy` 里的这些退出参数不再生效。
- `exit_owner: engine` 时,服务端价格风控生效;指标里的 `close_*` 应只表达趋势反转等结构性退出,不要再塞一套窄 tp/sl。
- 真正的问题是没人说得清「主退出」是指标触及,还是 `# @strategy` 服务端风控;`exit_owner` 就是为了解决这个边界。
### 11.8 `both` 模式下把止盈/止损全塞进 `buy` / `sell`
容易误解的写法:
```python
df['buy'] = entry_long | short_tp | short_sl
df['sell'] = entry_short | long_tp | long_sl
# @strategy tradeDirection both
```
需要建立的心智模型:
- `short_tp` / `short_sl` 进入 `buy` 后,在 `both` 下不是「仅平空」,而是**平空并可能开多**(与回测一致)。
- 若你期望「空侧止盈后保持空仓」,当前 `IndicatorStrategy` + `both` **不适合**,应改 `tradeDirection`、四路信号或 `ScriptStrategy`
- 实盘在 `exit_signal_mode=immediate` 时,这类退出可能比回测更早触发;要与回测对齐请用 `confirmed`(见 §3.6.1)。
### 11.9 实盘日志里 `invalid amount (0.0) for close_*`
常见原因:
1. 服务端止损/止盈/追踪止损已平仓,指标信号又提交了一次同向 `close_*`
2. 本地持仓表滞后于交易所;执行器会先 **sync + 按交易所持仓重算数量**,仍为空才拒单。
处理建议:
- 按 §11.7 避免双重退出;检查是否应改成 `# exit_owner: indicator`,或是否同时启用了 `trailingEnabled` 与指标内 tp/sl。
- 确认 `tradeDirection both` 下对 `buy`/`sell` 的解释符合 §3.3.1,不要期待单独的 `close_short` 列。
---
## 12. 平台支持字段速查表
这一节可以当成“当前平台到底支持什么”的速查页,写策略时可以直接对着查。
### 12.1 `# @strategy` 支持的 key
| 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 用于指标侧默认策略配置
- **数值单位统一为 0–1 小数比例**(与 `StrategyConfigParser`、回测、实盘一致)
- **止损/止盈/追踪按标的涨跌幅计算,不除以杠杆**(杠杆只影响盈亏金额与爆仓)
- **`entryPct 1` 表示 100% 可用资金开仓**,不是 1%
- 若声明 `# exit_owner: indicator``stopLossPct` / `takeProfitPct` / `trailing*` 在回测和实盘中都不会触发服务端平仓;退出以指标信号为准
- 若声明 `# exit_owner: engine`,这些服务端价格风控参数才会参与平仓
- 不要把 `leverage` 写进 `# @strategy`
- 交易所、标的、凭证、杠杆都应该放在产品配置层
### 12.2 契约头注释
| Key | 支持值 | 含义 |
|-----|--------|------|
| `signal_form` | `two_way` / `four_way` | 执行信号形态 |
| `exit_owner` | `indicator` / `engine` | 价格退出负责人;当前不支持 `layered` |
| `flip_mode` | `R1` / `R2` | 反手时序:R1 下一根再开;R2 同 bar 先平后开 |
推荐写在文件顶部:
```python
# signal_form: four_way
# exit_owner: indicator
# flip_mode: R1
```
### 12.3 `# @param` 速查格式
| 部分 | 示例 | 含义 |
|------|------|------|
| 名称 | `fast_len` | 参数键名 |
| 类型 | `int` / `float` / `bool` / `str` / `string` | 支持的类型 |
| 默认值 | `20` | 系统看到的默认值 |
| 描述 | `Fast EMA length` | 给人看的说明 |
示例:
```python
# @param fast_len int 20 Fast EMA length
# @param allow_short bool true Allow short entries
```
随后用下面这种方式读取:
```python
fast_len = int(params.get('fast_len', 20))
allow_short = bool(params.get('allow_short', True))
```
### 12.3 `ScriptStrategy` 里的 `ctx` 支持什么
| 项目 | 类型 | 含义 |
|------|------|------|
| `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` | 运行时数量镜像 |
### 12.4 `ScriptStrategy` 里的 `bar` 有哪些字段
| 字段 | 含义 |
|------|------|
| `bar.open` | 开盘价 |
| `bar.high` | 最高价 |
| `bar.low` | 最低价 |
| `bar.close` | 收盘价 |
| `bar.volume` | 成交量 |
| `bar.timestamp` | 当前运行时传入的时间值 |
### 12.5 `IndicatorStrategy` 的 `output` 允许哪些结构
顶层结构通常写成:
```python
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` |
### 12.6 快速提醒
- `df['buy']``df['sell']` 应该是布尔值,并与 `df` 长度完全对齐
- 尽量使用边缘触发信号
- 不要在信号逻辑里使用 `shift(-1)`
- 当规则语义明显是“全部退出”时,优先用 `ctx.close_position()`
- `amount` 更适合作为运行时下单意图,最终仓位仍应通过保存后的策略回测核实
---
## 13. 推荐开发流程
1. 先用 `IndicatorStrategy` 把想法原型化。
2. 先验证图表、信号密度和 next-bar-open 的回测语义。
3.`# @param``# @strategy` 元数据补完整。
4. 明确写清楚:退出到底是“信号负责”还是“引擎负责”。
5. 保存策略后,再从持久化记录跑策略回测。
6. 只有在确实需要运行时仓位管理时,再迁移到 `ScriptStrategy`
7. 确认配置、凭证和市场语义都正确后,再进入模拟盘或实盘。