1437 lines
50 KiB
Markdown
1437 lines
50 KiB
Markdown
# 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%** 触发(0–1 小数;`0.001` = 0.1%)
|
||
- `takeProfitPct`:止盈比例,例如 `0.06` 表示 **标的价格上涨 6%** 触发
|
||
- `entryPct`:开仓资金占比(0–1;**`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. 确认配置、凭证和市场语义都正确后,再进入模拟盘或实盘。
|
||
|