上传文件至「/」

This commit is contained in:
2026-07-11 20:13:58 +00:00
parent ac03d9e67b
commit 0c28e0ad97
5 changed files with 1467 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
# QuantDinger 策略与指标开发文档集
本目录包含 QuantDinger 量化交易平台的策略/指标开发相关文档与示例代码,覆盖从入门到进阶的完整开发链路。
---
## 目录结构
```
QuantDingerdocs/
├── README.md # 本文件
├── STRATEGY_DEV_GUIDE_CN.md # 策略开发指南(中文)
├── STRATEGY_DEV_GUIDE.md # 策略开发指南(英文)
├── SIGNAL_EXECUTION_STANDARD_CN.md # 信号与执行标准(中文)
├── SIGNAL_EXECUTION_STANDARD.md # 信号与执行标准(英文)
├── SCRIPT_CODE_DEVELOPMENT_GUIDE_CN.md # 脚本代码开发指南(中文)
├── SCRIPT_CODE_DEVELOPMENT_GUIDE_EN.md # 脚本代码开发指南(英文)
├── CROSS_SECTIONAL_STRATEGY_GUIDE_CN.md # 截面策略使用指南(中文)
├── CROSS_SECTIONAL_STRATEGY_GUIDE_EN.md # 截面策略使用指南(英文)
├── EXTENSION_GUIDE.md # 扩展指南(英文)
├── STRATEGY_RUNTIME_ALGO_TRADING_IMPLEMENTATION_PLAN_CN.md # 复杂策略运行时改造方案
├── STRATEGY_RUNTIME_ALGO_TRADING_UPGRADE_CN.md # 策略运行时与算法交易升级规划
├── dual_ma_with_params.py # 双均线策略示例
├── multi_indicator_composite.py # 多指标组合策略示例
├── template_four_way_edge.py # 四路信号模板
├── cross_sectional_momentum_rsi.py # 截面动量+RSI 示例
└── examples/ # 示例代码副本(同上四个 py 文件)
```
---
## 文档说明
### 核心必读
| 文档 | 说明 |
|------|------|
| **[信号与执行标准](SIGNAL_EXECUTION_STANDARD_CN.md)** | 平台级契约,定义策略信号的语义、回测/实盘对齐规则、两路/四路信号选型、退出负责人模型。**所有策略开发者必读**。 |
| **[策略开发指南](STRATEGY_DEV_GUIDE_CN.md)** | 站在开发者视角,讲解 `IndicatorStrategy`(指标策略)和 `ScriptStrategy`(脚本策略)两条开发路径的区别、适用场景和编写方法。 |
| **[脚本代码开发指南](SCRIPT_CODE_DEVELOPMENT_GUIDE_CN.md)** | 深入讲解 `ScriptStrategy``on_init/on_bar` 生命周期、状态管理、分批建仓、止盈止损等复杂场景的编写规范。 |
### 进阶专题
| 文档 | 说明 |
|------|------|
| **[截面策略指南](CROSS_SECTIONAL_STRATEGY_GUIDE_CN.md)** | 多标的截面策略(Cross-Sectional)的配置与使用说明,涵盖因子评分、排序、组合管理和定期调仓。 |
| **[扩展指南](EXTENSION_GUIDE.md)** | 后端扩展开发指南,覆盖 API 端点、Agent Gateway、服务层、数据适配器等模块的扩展规范。 |
| **[策略运行时改造方案](STRATEGY_RUNTIME_ALGO_TRADING_IMPLEMENTATION_PLAN_CN.md)** | 脚本策略从"信号生成"升级为"复杂状态机与算法执行"的实施方案,涉及分仓、马丁、均价止盈、重启恢复等。 |
| **[策略运行时升级规划](STRATEGY_RUNTIME_ALGO_TRADING_UPGRADE_CN.md)** | 对策略运行时与算法交易基础设施升级的规划建议,分析当前能力边界与未来演进方向。 |
---
## 示例代码
| 文件 | 说明 |
|------|------|
| **[dual_ma_with_params.py](dual_ma_with_params.py)** | 双均线交叉策略(四路信号),演示 `# @param``# @strategy` 元数据声明与边缘触发信号生成。 |
| **[multi_indicator_composite.py](multi_indicator_composite.py)** | 多指标组合策略,演示均线 + RSI + MACD + 成交量过滤的组合信号写法。 |
| **[template_four_way_edge.py](template_four_way_edge.py)** | 四路信号空壳模板,复制到指标 IDE 作为新策略起点,对齐信号执行标准 v1。 |
| **[cross_sectional_momentum_rsi.py](cross_sectional_momentum_rsi.py)** | 截面策略研究参考,演示动量 + RSI 复合因子对多标的打分排序思路。 |
---
## 推荐阅读顺序
```
1. SIGNAL_EXECUTION_STANDARD_CN.md → 理解平台合约与信号语义
2. STRATEGY_DEV_GUIDE_CN.md → 掌握 IndicatorStrategy / ScriptStrategy 两条路径
3. 示例代码(.py 文件) → 对照模板上手写策略
4. SCRIPT_CODE_DEVELOPMENT_GUIDE_CN.md → 深入脚本策略的复杂场景
5. CROSS_SECTIONAL_STRATEGY_GUIDE_CN.md → 需要多标的同时交易时阅读
6. 运行时相关文档 → 需要复杂状态机/算法执行时阅读
7. EXTENSION_GUIDE.md → 需要扩展后端功能时阅读
```
---
## 关键概念速览
### 两条开发路径
| | IndicatorStrategy | ScriptStrategy |
|------|------|------|
| 驱动方式 | 基于 `df` 批量计算指标序列 | 基于 `on_init/on_bar` 逐根事件驱动 |
| 适用场景 | 指标研究、信号原型、参数调优、信号型回测 | 有状态策略、分批建仓、动态仓位管理、实盘执行 |
| 信号输出 | 布尔型 `buy`/`sell`/`close_buy`/`close_sell` | 通过 `ctx.buy()`/`ctx.sell()`/`ctx.close_position()` 发出动作 |
### 四路信号形态
```
signal_form: four_way
├── buy → 开多信号
├── sell → 开空信号
├── close_buy → 平多信号(结构性反转)
└── close_sell → 平空信号(结构性反转)
```
> 退出负责人(exit_owner)为 `engine` 时,固定价格止盈止损由引擎管理;为 `indicator` 时,所有退出逻辑写入信号代码。
+531
View File
@@ -0,0 +1,531 @@
# QuantDinger 脚本代码开发指南
本文说明如何编写可回测、可实盘、可维护的 QuantDinger 脚本代码。脚本代码适合有运行状态的策略,例如分层建仓、篮子均价止盈、追踪止损、金字塔加仓、冷却期、重复下单防护等。
## 1. 核心边界
脚本代码只负责策略逻辑。用户运行时填写的内容不应该再写进代码参数。
运行面板负责:
- 标的,例如 BTC/USDT
- 市场类型,现货或合约
- 交易方向,做多、做空或双向
- 投入金额
- 合约杠杆
- 账户、通知、实盘风控开关
脚本代码负责:
- 入场条件
- 加仓、减仓、止盈、止损规则
- 分层数量、间距、倍数、周期、冷却期
- 状态持久化和防重复下单
- 日志与篮子状态 checkpoint
不要在脚本中写:
```python
ctx.param('direction', 'long')
ctx.param('market_type', 'swap')
ctx.param('investment_amount', 1000)
ctx.param('leverage', 3)
ctx.param('base_notional', 50)
```
这些值应由运行面板传入,可在脚本中读取:
```python
direction = ctx.direction
market_type = ctx.market_type
budget = ctx.investment_amount
leverage = ctx.leverage
```
## 2. 生命周期
脚本必须包含:
```python
def on_init(ctx):
pass
def on_bar(ctx, bar):
pass
```
`on_init(ctx)` 在脚本启动时执行一次,用于读取策略参数和初始化默认值。
`on_bar(ctx, bar)` 每根 K 线执行一次。当前统一使用 1m K 线作为脚本粒度;实盘还会每 10 秒检查一次最新价格,用于降低止盈止损响应延迟。回测只基于历史 K 线,因此极高频 tick 级逻辑不应该写在脚本里。
`bar` 支持:
```python
bar['open']
bar['high']
bar['low']
bar['close']
bar['volume']
bar['timestamp']
```
## 3. 参数设计
只把策略结构参数放进 `ctx.param(...)`
```python
def on_init(ctx):
ctx.fast_period = ctx.param('fast_period', 12)
ctx.slow_period = ctx.param('slow_period', 36)
ctx.take_profit_pct = ctx.param('take_profit_pct', 0.006)
ctx.max_layers = ctx.param('max_layers', 5)
```
百分比默认值在 Python 里使用 0-1 小数:
- `0.006` 表示 0.6%
- `0.02` 表示 2%
- `0.8` 表示 80%
前端可以用 0-100 的显示方式,但写回代码时应保持 Python 小数。
## 4. 投入金额和下单金额
推荐所有状态型脚本使用 `ctx.basket(side).open_child_order(..., notional=quote_amount)`
`notional` 表示计价货币金额,例如 USDT 金额。回测和实盘会根据市场类型、价格和杠杆换算为实际下单数量。
示例:
```python
def _run_budget(ctx):
try:
budget = float(ctx.investment_amount or 0.0)
except Exception:
budget = 0.0
if budget > 0:
return budget
return 0.0
def _planned_base_notional(ctx):
total_weight = 1 + 1.8 + 3.24
return _run_budget(ctx) / total_weight
def on_bar(ctx, bar):
price = float(bar['close'])
side = 'short' if str(ctx.direction).lower() == 'short' else 'long'
basket = ctx.basket(side)
quote_amount = _planned_base_notional(ctx)
basket.open_child_order(
layer=1,
order=1,
notional=quote_amount,
price=price,
action='open',
payload={'reason': 'first_entry'},
)
```
不要把“基础下单金额”作为模板参数暴露给用户。更清晰的模型是:用户填投入金额,脚本根据层数、倍数和总权重自动拆分每个子单的计划金额。
## 5. 方向处理
方向由运行面板选择。脚本只读取,不写死。
推荐写法:
```python
def _side(ctx):
try:
direction = str(ctx.direction)
except Exception:
direction = 'long'
return 'short' if direction.lower() == 'short' else 'long'
```
现货只能做多;这个限制由运行面板和执行层处理。脚本里不需要再做一套复杂分支。
## 6. 状态管理
有分层、篮子、冷却期的策略必须使用 `ctx.state`
常见状态:
- 当前层数:`layer`
- 当前子单:`order`
- 均价:`avg_cost`
- 总数量:`qty`
- 下一次触发价格:`next_trigger`
- 冷却到哪根 K 线:`cooldown_until`
- 上次下单 K 线:`last_order_bar`
示例:
```python
bar_no = int(ctx.current_index)
last_order_bar = int(ctx.state.get('last_order_bar', -999999) or -999999)
if last_order_bar == bar_no:
return
ctx.state.set('last_order_bar', bar_no)
```
这样可以防止同一根 K 线重复发单。
## 7. Basket API
`ctx.basket(side)` 适合所有有篮子概念的策略。
常用方法:
```python
basket = ctx.basket('long')
basket.open_child_order(
layer=1,
order=1,
notional=50,
price=price,
action='open',
payload={'reason': 'entry'},
)
basket.open_child_order(
layer=2,
order=1,
notional=80,
price=price,
action='add',
payload={'reason': 'add_layer'},
)
basket.close_all(reason='take_profit')
```
`checkpoint` 用于展示策略当前状态:
```python
basket.checkpoint(
status='opening',
current_layer=layer,
current_order_in_layer=order,
total_qty=qty,
total_notional=qty * avg_cost,
avg_entry_price=avg_cost,
next_entry_trigger=next_trigger,
take_profit_price=take_profit,
max_layer=max_layers,
max_orders_per_layer=orders_per_layer,
)
```
## 8. 回测和实盘对齐
脚本回测和实盘使用同一套 `on_init/on_bar` 语义。需要注意:
- 回测基于历史 K 线,不模拟每 10 秒 tick 细节。
- 实盘会额外拉取最新价格用于更及时地检查价格条件。
- 下单金额建议使用 basket `notional`,这样合约杠杆在回测和实盘中口径一致。
- 策略不能依赖未来数据,只能使用 `ctx.bars(n)` 获取当前及历史 K 线。
- 每次加仓必须有价格间距、最大层数和防重复下单。
## 9. 沙箱限制
脚本在安全沙箱中执行。不要使用:
- `getattr``setattr``delattr`
- `eval``exec``open``compile`
- `globals``vars``dir`
- `__builtins__`、dunder 属性
- 文件、网络、数据库、进程、线程相关 API
- `os``sys``requests``urllib``socket``subprocess``threading``multiprocessing``sqlite3``psycopg``sqlalchemy``pathlib``tempfile``glob``io``operator``pickle``ctypes`
读取可选字段时,用 `try/except` 直接访问:
```python
try:
direction = str(ctx.direction)
except Exception:
direction = 'long'
```
不要写:
```python
direction = getattr(ctx, 'direction', 'long')
```
## 10. 推荐模板类型
经典可落地的脚本模板包括:
- EMA ATR 趋势风控:趋势跟随、ATR 止损、ATR 追踪止损。
- Donchian 突破金字塔:通道突破入场,盈利后顺势加仓。
- 布林均值回归篮子:触及布林外轨后分层建仓,均价回归止盈。
- 阶梯分仓马丁篮子:多层分仓、子单马丁、均价止盈、硬止损。
这些模板覆盖趋势、突破、震荡均值回归和高风险篮子模型,避免把多个相似的“加仓模板”重复展示给用户。
## 11. 复杂策略的推荐架构
复杂脚本不要写成一大坨 `if/else`。推荐拆成五层:
1. 信号层:判断是否允许入场、是否允许加仓、是否应该退出。
2. 资金层:把运行面板的投入金额拆成每一层、每一单的计划金额。
3. 状态层:保存当前阶段、层数、子单、均价、触发价、冷却期。
4. 执行层:只负责发出 `basket.open_child_order``basket.close_all`
5. 风控层:硬止损、最大层数、最大订单数、冷却期、同 K 线防重复。
推荐文件结构虽然仍然是一个脚本,但代码内部应保持这种顺序:
```python
def on_init(ctx):
# 1. 读取策略参数
pass
def _side(ctx):
# 2. 读取运行面板方向
pass
def _run_budget(ctx):
# 3. 读取运行面板投入金额
pass
def _planned_notional(ctx, layer, order):
# 4. 计算每一单计划金额
pass
def _entry_signal(ctx, bar, bars):
# 5. 入场信号
pass
def _risk_exit(ctx, price, avg_cost):
# 6. 风控退出
pass
def _place_child(ctx, basket, layer, order, price, action):
# 7. 发单并更新状态
pass
def on_bar(ctx, bar):
# 8. 主流程,只编排,不塞复杂公式
pass
```
这样用户后续加新条件、换资金模型、换退出逻辑时,不需要重写整份策略。
## 12. 状态机模型
复杂策略应该先设计状态机。推荐状态:
| 状态 | 含义 | 允许动作 |
| --- | --- | --- |
| `idle` | 空仓等待 | 判断入场 |
| `opening` | 已开首单或正在建仓 | 加仓、止盈、止损 |
| `active` | 仓位完整运行中 | 止盈、止损、追踪退出 |
| `closing` | 已触发平仓 | 等待执行层完成 |
| `cooldown` | 平仓后冷却 | 不允许重新入场 |
脚本里可以不用显式保存字符串状态,但逻辑上必须有这套概念。最少要保存:
```python
ctx.state.set('layer', layer)
ctx.state.set('order', order)
ctx.state.set('avg_cost', avg_cost)
ctx.state.set('qty', qty)
ctx.state.set('next_trigger', next_trigger)
ctx.state.set('cooldown_until', cooldown_until)
ctx.state.set('last_order_bar', bar_no)
```
主流程建议固定为:
```python
def on_bar(ctx, bar):
# 1. 数据不足直接返回
# 2. 读取运行面板方向、价格、状态
# 3. 如果空仓,检查是否需要重置状态
# 4. 如果冷却中,直接返回
# 5. 如果空仓,判断入场
# 6. 如果持仓,先判断止盈/止损
# 7. 如果仍持仓,再判断加仓
# 8. 发单后更新 checkpoint 和 state
```
不要先加仓再判断止损。复杂策略里这个顺序很重要。
## 13. 资金拆分模型
用户只填一个“投入金额”。脚本需要根据策略结构拆分每个子单。
### 13.1 等额分层
适合突破金字塔:
```python
base_notional = ctx.investment_amount / ctx.max_layers
```
如果投入 1000 USDT、4 层,则每层 250 USDT。
### 13.2 温和递增分层
适合布林均值回归:
```python
weights = [ctx.layer_multiplier ** i for i in range(ctx.max_layers)]
base_notional = ctx.investment_amount / sum(weights)
layer_notional = base_notional * (ctx.layer_multiplier ** (layer - 1))
```
如果投入 1000 USDT、4 层、倍数 1.25,则每层按权重 1、1.25、1.56、1.95 拆分。
### 13.3 分仓马丁
适合 5 个分仓、每仓 3 单:
```python
one_layer_weight = sum([ctx.martingale_multiplier ** i for i in range(ctx.orders_per_layer)])
total_weight = one_layer_weight * ctx.max_layers
base_notional = ctx.investment_amount / total_weight
child_notional = base_notional * (ctx.martingale_multiplier ** (order - 1))
```
如果投入金额是 1000 USDT、5 层、每层 3 单、倍数 1.8:
- 单层权重:`1 + 1.8 + 3.24 = 6.04`
- 总权重:`6.04 * 5 = 30.2`
- 基础子单金额:`1000 / 30.2 = 33.11 USDT`
- 每层三单约为:`33.11 / 59.60 / 107.27 USDT`
这能避免“代码里写了基础下单金额,用户又填投入金额”的冲突。
## 14. 信号层设计
复杂策略的入场信号最好是“事件”,不是“状态”。
不推荐:
```python
if not has_position and fast > slow:
open_position()
```
这样会在趋势状态持续时频繁尝试入场,必须依赖其他防线兜底。
推荐:
```python
cross_up = prev_fast <= prev_slow and fast > slow
if not has_position and cross_up:
open_position()
```
常见信号模型:
- 趋势:EMA 金叉/死叉、价格突破均线并确认。
- 突破:当前收盘价突破过去 N 根最高/最低。
- 均值回归:价格触及布林带外轨,并用 RSI 过滤极端状态。
- 分仓马丁:首单可以无信号启动,也可以加趋势/震荡过滤;后续加仓只能由价格间距触发。
## 15. 风控层设计
复杂策略必须至少有三类风控:
1. 结构风控:最大层数、每层最大子单数、单 K 线只允许一次下单。
2. 价格风控:硬止损、均价止盈、追踪止损、通道退出。
3. 时间风控:冷却期、超时退出、长时间未成交后的状态重置。
示例:
```python
if last_order_bar == bar_no:
return
if layer >= ctx.max_layers and order >= ctx.orders_per_layer:
# 不再继续加仓,只等待止盈或硬止损
return
if pnl <= -ctx.hard_stop_pct:
basket.close_all(reason='hard_stop')
_reset_cycle(ctx, bar_no + ctx.cooldown_bars)
return
```
## 16. 回测前检查清单
用户写完复杂脚本后,先检查:
- 是否定义了 `on_init(ctx)``on_bar(ctx, bar)`
- 是否没有使用 `getattr`、文件、网络、数据库、进程等沙箱禁止能力。
- 是否没有把方向、市场类型、投入金额、杠杆写成 `ctx.param`
- 是否所有百分比参数在代码里都是 0-1 小数。
- 是否所有加仓都有最大次数和价格间距。
- 是否所有发单都有同 K 线防重复。
- 是否下单金额来自 `ctx.investment_amount` 的拆分。
- 是否使用 `basket.open_child_order(..., notional=...)` 表示计价金额。
- 是否在止盈/止损后重置状态并进入冷却。
- 是否能解释每一个状态变量的含义。
## 17. 实盘前检查清单
回测通过不等于可以直接实盘。实盘前还要检查:
- 现货策略是否只做多。
- 合约策略的杠杆是否符合交易所限制。
- 投入金额是否足够覆盖最大计划层数。
- 最大单数是否会超过交易所限频或最小下单金额限制。
- 硬止损是否存在,且不是过大到没有意义。
- 是否设置了账户级风控、通知和异常停机策略。
- 是否用小金额或模拟盘跑过至少一个完整开仓、加仓、平仓周期。
## 18. AI 助手提示词建议
让 AI 生成复杂脚本时,提示词要明确边界。推荐这样写:
```text
写一个 QuantDinger 脚本代码:
1. 使用 on_init/on_bar。
2. 运行面板负责标的、现货/合约、方向、投入金额和杠杆,代码里不要写成 ctx.param。
3. 使用 ctx.basket(side).open_child_order(..., notional=...) 下单。
4. 投入金额按 5 层、每层 3 单、马丁倍数 1.8 自动拆分。
5. 每个子单之间有价格间距,均价止盈,硬止损,同 K 线防重复。
6. 不要使用 getattr、文件、网络、数据库、导入危险模块。
```
如果要改现有模板,建议说:
```text
基于当前模板,只调整参数和风控,不要改变运行面板边界。
把止盈改为 0.8%,层间距改成逐层扩大,最大亏损 12% 硬止损。
```
## 19. 最小骨架
```python
"""
My Script Strategy
"""
def on_init(ctx):
ctx.lookback = ctx.param('lookback', 20)
ctx.take_profit_pct = ctx.param('take_profit_pct', 0.01)
def _side(ctx):
try:
direction = str(ctx.direction)
except Exception:
direction = 'long'
return 'short' if direction.lower() == 'short' else 'long'
def on_bar(ctx, bar):
bars = ctx.bars(ctx.lookback + 1)
if len(bars) < ctx.lookback + 1:
return
side = _side(ctx)
price = float(bar['close'])
basket = ctx.basket(side)
# Add entry, state, risk, and exit logic here.
```
+531
View File
@@ -0,0 +1,531 @@
# QuantDinger Script Code Development Guide
This guide explains how to write QuantDinger script code that can be backtested, deployed live, and maintained safely. Script code is for stateful strategies such as layered entries, basket average-cost take profit, trailing stops, pyramiding, cooldowns, and duplicate-order protection.
## 1. Core Boundary
Script code owns strategy logic only. Values chosen by the user at run time should not be duplicated as code parameters.
The run panel owns:
- Symbol, such as BTC/USDT
- Market type, spot or swap
- Trade direction, long, short, or both
- Investment amount
- Contract leverage
- Account, notifications, and live risk controls
Script code owns:
- Entry conditions
- Add, reduce, take-profit, and stop-loss rules
- Layer count, spacing, multipliers, periods, and cooldowns
- Persistent state and duplicate-order protection
- Logs and basket checkpoints
Do not write these as script parameters:
```python
ctx.param('direction', 'long')
ctx.param('market_type', 'swap')
ctx.param('investment_amount', 1000)
ctx.param('leverage', 3)
ctx.param('base_notional', 50)
```
Read run-panel values directly when needed:
```python
direction = ctx.direction
market_type = ctx.market_type
budget = ctx.investment_amount
leverage = ctx.leverage
```
## 2. Lifecycle
Every script must define:
```python
def on_init(ctx):
pass
def on_bar(ctx, bar):
pass
```
`on_init(ctx)` runs once when the script starts. Use it to read strategy parameters and initialize defaults.
`on_bar(ctx, bar)` runs once per K-line bar. The platform uses a fixed 1m bar stream for script strategies; live execution also checks the latest price every 10 seconds to reduce take-profit and stop-loss latency. Backtests are K-line based, so do not write tick-level strategies that require every market tick.
`bar` supports:
```python
bar['open']
bar['high']
bar['low']
bar['close']
bar['volume']
bar['timestamp']
```
## 3. Parameter Design
Only put strategy knobs into `ctx.param(...)`:
```python
def on_init(ctx):
ctx.fast_period = ctx.param('fast_period', 12)
ctx.slow_period = ctx.param('slow_period', 36)
ctx.take_profit_pct = ctx.param('take_profit_pct', 0.006)
ctx.max_layers = ctx.param('max_layers', 5)
```
Percent defaults in Python code use 0-1 ratios:
- `0.006` means 0.6%
- `0.02` means 2%
- `0.8` means 80%
The frontend may display 0-100 percent values, but the generated Python default literal should remain a ratio.
## 4. Investment Amount and Order Sizing
For stateful scripts, prefer `ctx.basket(side).open_child_order(..., notional=quote_amount)`.
`notional` means quote-currency amount, for example USDT. Backtest and live execution convert it into base quantity using market type, price, and leverage.
Example:
```python
def _run_budget(ctx):
try:
budget = float(ctx.investment_amount or 0.0)
except Exception:
budget = 0.0
if budget > 0:
return budget
return 0.0
def _planned_base_notional(ctx):
total_weight = 1 + 1.8 + 3.24
return _run_budget(ctx) / total_weight
def on_bar(ctx, bar):
price = float(bar['close'])
side = 'short' if str(ctx.direction).lower() == 'short' else 'long'
basket = ctx.basket(side)
quote_amount = _planned_base_notional(ctx)
basket.open_child_order(
layer=1,
order=1,
notional=quote_amount,
price=price,
action='open',
payload={'reason': 'first_entry'},
)
```
Do not expose "base order amount" as a template parameter. A clearer model is: the user enters one investment amount, and the script derives child order amounts from layer count, multipliers, and total weights.
## 5. Direction Handling
Direction is selected in the run panel. The script reads it, but should not hard-code it.
Recommended helper:
```python
def _side(ctx):
try:
direction = str(ctx.direction)
except Exception:
direction = 'long'
return 'short' if direction.lower() == 'short' else 'long'
```
Spot can only run long. That constraint is enforced by the run panel and execution layer; scripts do not need a second complicated branch.
## 6. State Management
Layered, basket, or cooldown-based strategies must use `ctx.state`.
Common state keys:
- Current layer: `layer`
- Current child order: `order`
- Average cost: `avg_cost`
- Total quantity: `qty`
- Next trigger price: `next_trigger`
- Cooldown-until bar: `cooldown_until`
- Last order bar: `last_order_bar`
Example:
```python
bar_no = int(ctx.current_index)
last_order_bar = int(ctx.state.get('last_order_bar', -999999) or -999999)
if last_order_bar == bar_no:
return
ctx.state.set('last_order_bar', bar_no)
```
This prevents duplicate orders on the same bar.
## 7. Basket API
`ctx.basket(side)` is the preferred API for strategies with basket semantics.
Common usage:
```python
basket = ctx.basket('long')
basket.open_child_order(
layer=1,
order=1,
notional=50,
price=price,
action='open',
payload={'reason': 'entry'},
)
basket.open_child_order(
layer=2,
order=1,
notional=80,
price=price,
action='add',
payload={'reason': 'add_layer'},
)
basket.close_all(reason='take_profit')
```
Use `checkpoint` to expose the current strategy state:
```python
basket.checkpoint(
status='opening',
current_layer=layer,
current_order_in_layer=order,
total_qty=qty,
total_notional=qty * avg_cost,
avg_entry_price=avg_cost,
next_entry_trigger=next_trigger,
take_profit_price=take_profit,
max_layer=max_layers,
max_orders_per_layer=orders_per_layer,
)
```
## 8. Backtest and Live Alignment
Script backtest and live execution share the same `on_init/on_bar` semantics. Keep these points in mind:
- Backtests are based on historical K-line bars and do not simulate every 10-second live price check.
- Live execution additionally checks latest price for faster price-condition handling.
- Use basket `notional` sizing so leverage behavior stays consistent across backtest and live.
- Do not use future data. Use `ctx.bars(n)` for current and historical bars only.
- Every scale-in needs a price distance, max layer/order limit, and duplicate-order guard.
## 9. Sandbox Restrictions
Scripts run inside a safety sandbox. Do not use:
- `getattr`, `setattr`, `delattr`
- `eval`, `exec`, `open`, `compile`
- `globals`, `vars`, `dir`
- `__builtins__` or dunder attributes
- File, network, database, process, or thread APIs
- `os`, `sys`, `requests`, `urllib`, `socket`, `subprocess`, `threading`, `multiprocessing`, `sqlite3`, `psycopg`, `sqlalchemy`, `pathlib`, `tempfile`, `glob`, `io`, `operator`, `pickle`, or `ctypes`
For optional fields, use direct access with `try/except`:
```python
try:
direction = str(ctx.direction)
except Exception:
direction = 'long'
```
Do not write:
```python
direction = getattr(ctx, 'direction', 'long')
```
## 10. Recommended Template Types
Professional script templates should cover different market regimes instead of repeating the same scale-in idea:
- EMA ATR trend risk: trend following with ATR hard stop and ATR trailing stop.
- Donchian breakout pyramid: channel breakout entry and favorable pyramiding.
- Bollinger mean reversion basket: layered entries around volatility-band extremes and average-cost reversion exit.
- Layered basket martingale: multiple layers, martingale child sizing, average-cost take profit, and hard stop.
These four cover trend, breakout, range reversion, and high-risk basket models without showing several duplicate "add-on-dip" templates.
## 11. Recommended Architecture for Complex Scripts
Do not write complex scripts as one large `if/else` block. Split the logic into five layers:
1. Signal layer: decide whether entry, add, or exit is allowed.
2. Sizing layer: split the run-panel investment amount into planned child order amounts.
3. State layer: persist phase, layer, child order, average cost, trigger price, and cooldown.
4. Execution layer: emit only `basket.open_child_order` or `basket.close_all`.
5. Risk layer: hard stop, max layers, max child orders, cooldown, and duplicate-order guards.
The script is still a single file, but the internal structure should look like this:
```python
def on_init(ctx):
# 1. Read strategy knobs
pass
def _side(ctx):
# 2. Read run-panel direction
pass
def _run_budget(ctx):
# 3. Read run-panel investment amount
pass
def _planned_notional(ctx, layer, order):
# 4. Calculate planned quote amount for each child order
pass
def _entry_signal(ctx, bar, bars):
# 5. Entry signal
pass
def _risk_exit(ctx, price, avg_cost):
# 6. Risk exit
pass
def _place_child(ctx, basket, layer, order, price, action):
# 7. Place order and update state
pass
def on_bar(ctx, bar):
# 8. Main orchestration only
pass
```
This makes it much easier to add conditions, change sizing, or replace exit logic without rewriting the whole strategy.
## 12. State Machine Model
Complex strategies should start with a state machine design. Recommended phases:
| State | Meaning | Allowed actions |
| --- | --- | --- |
| `idle` | Flat and waiting | Check entry |
| `opening` | First order placed or basket building | Add, take profit, stop |
| `active` | Position is running | Take profit, stop, trailing exit |
| `closing` | Close triggered | Wait for execution layer |
| `cooldown` | Post-exit cooldown | No re-entry |
The script does not have to store these exact strings, but the logic should follow this model. At minimum persist:
```python
ctx.state.set('layer', layer)
ctx.state.set('order', order)
ctx.state.set('avg_cost', avg_cost)
ctx.state.set('qty', qty)
ctx.state.set('next_trigger', next_trigger)
ctx.state.set('cooldown_until', cooldown_until)
ctx.state.set('last_order_bar', bar_no)
```
Recommended `on_bar` flow:
```python
def on_bar(ctx, bar):
# 1. Return if not enough data
# 2. Read direction, price, and state
# 3. If flat, repair/reset stale state
# 4. If in cooldown, return
# 5. If flat, check entry
# 6. If in position, check take-profit/stop first
# 7. If still in position, check add logic
# 8. After any order, update checkpoint and state
```
Do not add first and check stop later. In complex scripts, this order matters.
## 13. Sizing Models
Users enter one investment amount. The script should split it according to the strategy structure.
### 13.1 Equal Layers
Useful for breakout pyramids:
```python
base_notional = ctx.investment_amount / ctx.max_layers
```
If investment is 1000 USDT and there are 4 layers, each layer is 250 USDT.
### 13.2 Mild Geometric Layers
Useful for Bollinger mean reversion:
```python
weights = [ctx.layer_multiplier ** i for i in range(ctx.max_layers)]
base_notional = ctx.investment_amount / sum(weights)
layer_notional = base_notional * (ctx.layer_multiplier ** (layer - 1))
```
If investment is 1000 USDT, 4 layers, multiplier 1.25, weights are 1, 1.25, 1.56, and 1.95.
### 13.3 Layered Martingale
Useful for 5 layers and 3 child orders per layer:
```python
one_layer_weight = sum([ctx.martingale_multiplier ** i for i in range(ctx.orders_per_layer)])
total_weight = one_layer_weight * ctx.max_layers
base_notional = ctx.investment_amount / total_weight
child_notional = base_notional * (ctx.martingale_multiplier ** (order - 1))
```
If investment is 1000 USDT, 5 layers, 3 orders per layer, multiplier 1.8:
- One layer weight: `1 + 1.8 + 3.24 = 6.04`
- Total weight: `6.04 * 5 = 30.2`
- Base child order amount: `1000 / 30.2 = 33.11 USDT`
- Each layer has roughly `33.11 / 59.60 / 107.27 USDT`
This removes the conflict between "base order amount in code" and "investment amount in the run panel".
## 14. Signal Layer Design
Complex entries should usually be events, not persistent states.
Not recommended:
```python
if not has_position and fast > slow:
open_position()
```
That condition remains true for many bars and relies on other guards to avoid repeated entry attempts.
Recommended:
```python
cross_up = prev_fast <= prev_slow and fast > slow
if not has_position and cross_up:
open_position()
```
Common signal models:
- Trend: EMA cross, price crossing and confirming above/below a moving average.
- Breakout: close breaks the highest high or lowest low of the past N bars.
- Mean reversion: price touches an outer Bollinger band with RSI confirmation.
- Layered martingale: first order may start immediately or use a filter; later orders must be triggered by price distance only.
## 15. Risk Layer Design
Every complex strategy needs at least three types of risk control:
1. Structure risk: max layers, max child orders, one order per bar.
2. Price risk: hard stop, average-cost take profit, trailing stop, channel exit.
3. Time risk: cooldown, timeout exit, stale-state reset.
Example:
```python
if last_order_bar == bar_no:
return
if layer >= ctx.max_layers and order >= ctx.orders_per_layer:
# No more adds. Wait for take-profit or hard stop.
return
if pnl <= -ctx.hard_stop_pct:
basket.close_all(reason='hard_stop')
_reset_cycle(ctx, bar_no + ctx.cooldown_bars)
return
```
## 16. Pre-Backtest Checklist
Before running a complex script backtest, check:
- `on_init(ctx)` and `on_bar(ctx, bar)` are defined.
- No `getattr`, file, network, database, process, or other sandbox-blocked capability is used.
- Direction, market type, investment amount, and leverage are not defined as `ctx.param`.
- Percent parameters in Python are 0-1 ratios.
- Every add has max count and price distance.
- Every order has a same-bar duplicate guard.
- Order sizing is derived from `ctx.investment_amount`.
- Orders use `basket.open_child_order(..., notional=...)` for quote amount sizing.
- State is reset after take-profit/stop and cooldown is set.
- Every state variable can be explained.
## 17. Pre-Live Checklist
A successful backtest does not mean the script is ready for live trading. Also check:
- Spot scripts only run long.
- Contract leverage fits exchange limits.
- Investment amount can cover the maximum planned layers.
- Max order count does not violate exchange rate limits or minimum order size.
- Hard stop exists and is not so wide that it is meaningless.
- Account-level risk controls, notifications, and emergency stop are configured.
- The script has completed at least one full open/add/close cycle with small capital or demo trading.
## 18. AI Assistant Prompt Examples
When asking AI to generate complex script code, state the boundary explicitly:
```text
Write a QuantDinger script code:
1. Use on_init/on_bar.
2. The run panel owns symbol, spot/swap, direction, investment amount, and leverage. Do not create ctx.param for these.
3. Use ctx.basket(side).open_child_order(..., notional=...) for orders.
4. Split the investment amount into 5 layers, 3 child orders per layer, martingale multiplier 1.8.
5. Use child-order spacing, average-cost take profit, hard stop, and same-bar duplicate protection.
6. Do not use getattr, file/network/database APIs, or unsafe imports.
```
When modifying an existing template:
```text
Based on the current template, only adjust parameters and risk controls. Do not change the run-panel boundary.
Change take-profit to 0.8%, make inter-layer spacing grow with depth, and add a 12% hard stop.
```
## 19. Minimal Skeleton
```python
"""
My Script Strategy
"""
def on_init(ctx):
ctx.lookback = ctx.param('lookback', 20)
ctx.take_profit_pct = ctx.param('take_profit_pct', 0.01)
def _side(ctx):
try:
direction = str(ctx.direction)
except Exception:
direction = 'long'
return 'short' if direction.lower() == 'short' else 'long'
def on_bar(ctx, bar):
bars = ctx.bars(ctx.lookback + 1)
if len(bars) < ctx.lookback + 1:
return
side = _side(ctx)
price = float(bar['close'])
basket = ctx.basket(side)
# Add entry, state, risk, and exit logic here.
```
+182
View File
@@ -0,0 +1,182 @@
# QuantDinger Signal & Execution Standard (SSOT)
**Version**: 1.0
**Status**: Current
**Scope**: All `IndicatorStrategy` flows (indicator Python saved as strategies) for backtest and live
**Implementation**: `BacktestService`, `TradingExecutor`, `validate_code_safety` / `verifyCode`
**Guide**: [STRATEGY_DEV_GUIDE.md](./STRATEGY_DEV_GUIDE.md)
---
## 1. Purpose
The platform runs **many different strategies**. Without a single contract, teams see:
- Different meanings of `buy`/`sell` across scripts;
- Backtests filling on **closed bar → next open** while live trades on **forming bars + immediate exits**;
- Indicator exits plus `# @strategy trailingEnabled` → duplicate closes and `invalid amount (0.0)` rejects.
This document is the **single source of truth** for what authors write and how engines interpret it.
New strategies **SHOULD** comply; existing ones **SHOULD** migrate per §9.
---
## 2. Terms
| Term | Meaning |
|------|---------|
| **Signal bar** | Bar where a boolean flag becomes true (timestamp = that bars close) |
| **Fill bar** | Bar where execution is anchored (default: bar after signal bar) |
| **Edge trigger** | true only on false→true transition vs previous bar |
| **Repaint** | Conditions on the forming bar flip as price updates |
| **Exit owner** | One primary close path: indicator signals **or** engine risk, not both narrow rules |
---
## 3. Choose a strategy form first
| Form | Use when | Avoid when |
|------|----------|------------|
| **A. Two-way** `buy` / `sell` | Simple crossovers; symmetric reversal | Separate tp/sl per side, state machines, “close only” semantics |
| **B. Four-way** `open_long` / `close_long` / `open_short` / `close_short` | State machines, touch exits, explicit queue alignment | — (recommended default for new strategies) |
| **C. ScriptStrategy** `on_bar` + `ctx` | Position-aware bots, scaling, cooldowns | Pure vectorized signals on full `df` |
**Platform default for new listings**: **Form B (four-way)**.
---
## 4. Signal output rules (IndicatorStrategy)
### 4.1 Universal MUST
1. **MUST** start mutations with `df = df.copy()`.
2. **MUST** define `output` (`name`, `plots`; optional `signals` for markers only).
3. **MUST** execution columns: length `len(df)`, dtype bool after `fillna(False).astype(bool)`.
4. **MUST** edge-trigger execution columns (§4.3) unless documented otherwise.
5. **MUST NOT** use `shift(-1)` or any look-ahead.
6. **MUST NOT** mix form A and form B execution columns as fill drivers (may keep `buy`/`sell` all-false for legacy UI).
### 4.2 Form A: `buy` / `sell`
| `tradeDirection` | `buy=True` | `sell=True` |
|------------------|------------|-------------|
| `long` | open long | close long |
| `short` | close short | open short |
| `both` | open long; **close short first if short** | open short; **close long first if long** |
**MUST NOT** treat `buy` as “close short only” under `both`. Use form B `close_*` for flat-only exits.
### 4.3 Form B: Four-way (recommended)
| Column | Meaning |
|--------|---------|
| `open_long` | Open / add long |
| `close_long` | Close long |
| `open_short` | Open / add short |
| `close_short` | Close short |
**MUST**: all four columns exist as bool.
**SHOULD**: same-bar priority close before open; avoid simultaneous `open_long` and `open_short` on one bar.
Fills follow **explicit four-way** semantics (not buy/sell both-mode remapping).
### 4.4 `tradeDirection`
`long` / `short` / `both` filters which legs are active; it does not replace four-way columns.
---
## 5. Exit ownership
Each strategy **MUST** declare one owner in comments or docs:
| Mode | Script | `# @strategy` |
|------|--------|---------------|
| **Indicator exits** `exit_owner: indicator` | `close_*` or equivalent explicit signal exits | `trailingEnabled false`; do not rely on SL/TP/trailing |
| **Engine exits** `exit_owner: engine` | entries only, or structural reverse `close_*` signals | SL/TP/trailing as needed |
**MUST NOT** combine tight indicator tp/sl with tight `trailingEnabled` on the same leg.
The current backend supports only `exit_owner: indicator` and `exit_owner: engine`. Do not emit `exit_owner: layered`; a mixed “indicator exits + engine backup” mode needs an explicit product/runtime change first.
---
## 6. Execution contract (backtest ↔ live)
### 6.1 Signal timing
| Setting | Standard |
|---------|----------|
| `signal_mode` | **`confirmed`** |
| `exit_signal_mode` | **`confirmed`** |
Forming-bar evaluation is for display/research only by default.
### 6.2 Fill timing
Default: **`next_bar_open`** (signal on bar *t*, fill on bar *t+1* open ± slippage).
### 6.3 Same-bar ordering
Priority: `close_*` > `reduce_*` > `open_*` > `add_*`.
Document flip mode **R1** (close on bar *t*, open on *t+1*, recommended) or **R2** (same-bar flip).
### 6.4 Close sizing
Sync positions → resolve size from DB and exchange → retry once if zero → fail with log if still zero.
---
## 7. Configuration
Use `# @strategy` for defaults; leverage and credentials stay in product UI.
Optional header block:
```python
# --- QuantDinger execution contract (v1) ---
# signal_form: four_way
# exit_owner: indicator
# flip_mode: R1
# @strategy tradeDirection both
# @strategy trailingEnabled false
```
---
## 8. Release checklist
- Form A/B/C chosen and documented
- Exit owner declared
- `confirmed` modes for live
- Backtest fills reviewed (signal bar vs fill bar)
- Pilot live run without systematic zero-amount closes
---
## 9. Migration tiers
**P0**: `both` + merged tp/sl in `buy`/`sell` + trailing → fix first.
**P1**: Touch logic + large live/backtest drift → four-way + confirmed + edge.
**P2**: Simple two-way → keep A, add contract header.
Optional badge: “Contract v1 certified”.
---
## 10. Implementation map
| Rule | Code |
|------|------|
| Four-way | `TradingExecutor._execute_indicator_with_prices`, `BacktestService` |
| Two-way both | `_indicator_both_mode` only for buy/sell normalization |
| Confirmed bars | `signal_mode`, `exit_signal_mode` |
| Close retry | `PendingOrderWorker`, `resolve_reduce_only_quantity` |
| Sandbox | `safe_exec.py` |
---
## 11. Revision history
| Version | Date | Notes |
|---------|------|-------|
| 1.0 | 2026-05 | Initial standard |
+125
View File
@@ -0,0 +1,125 @@
# ============================================================
# 多指标组合策略(文档同步版)
# Multi-Indicator Composite Strategy (Doc-Aligned Example)
# ============================================================
#
# 示例目标:
# 1. 演示如何把 `# @param`、`# @strategy` 和平台 UI 对齐
# 2. 演示如何组合均线、RSI、MACD、成交量过滤
# 3. 演示如何把原始条件整理成更稳定的边缘触发信号
#
# ============================================================
my_indicator_name = "多指标组合策略"
my_indicator_description = "均线、RSI、MACD 与成交量过滤共同参与的组合信号示例。"
# --- QuantDinger execution contract (v1) ---
# signal_form: four_way
# exit_owner: engine
# flip_mode: R2
# === 参数声明 ===
# @param sma_short int 10 短期均线周期
# @param sma_long int 30 长期均线周期
# @param rsi_period int 14 RSI周期
# @param rsi_oversold int 30 RSI超卖阈值
# @param rsi_overbought int 70 RSI超买阈值
# @param use_macd bool true 是否使用MACD过滤
# @param use_volume bool false 是否使用成交量过滤
# @param volume_mult float 1.5 成交量放大倍数
# === 平台默认策略配置 ===
# @strategy stopLossPct 0.025
# @strategy takeProfitPct 0.06
# @strategy entryPct 0.2
# @strategy trailingEnabled true
# @strategy trailingStopPct 0.02
# @strategy trailingActivationPct 0.04
# @strategy tradeDirection both
# 说明:本示例使用四路执行信号;固定止损、止盈和追踪止损由引擎负责。
# output["signals"] 只负责图表标记,不参与下单。
df = df.copy()
# === 获取参数 ===
sma_short_period = int(params.get('sma_short', 10))
sma_long_period = int(params.get('sma_long', 30))
rsi_period = int(params.get('rsi_period', 14))
rsi_oversold = int(params.get('rsi_oversold', 30))
rsi_overbought = int(params.get('rsi_overbought', 70))
use_macd = bool(params.get('use_macd', True))
use_volume = bool(params.get('use_volume', False))
volume_mult = float(params.get('volume_mult', 1.5))
# === 计算均线 ===
sma_short = df["close"].rolling(sma_short_period).mean()
sma_long = df["close"].rolling(sma_long_period).mean()
# === 计算RSI ===
delta = df["close"].diff()
gain = delta.where(delta > 0, 0).rolling(window=rsi_period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=rsi_period).mean()
rs = gain / loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
# === 计算MACD ===
exp1 = df["close"].ewm(span=12, adjust=False).mean()
exp2 = df["close"].ewm(span=26, adjust=False).mean()
macd = exp1 - exp2
macd_signal = macd.ewm(span=9, adjust=False).mean()
# === 计算成交量均线 ===
volume_ma = df["volume"].rolling(20).mean()
# === 原始条件 ===
ma_golden = (sma_short > sma_long) & (sma_short.shift(1) <= sma_long.shift(1))
ma_death = (sma_short < sma_long) & (sma_short.shift(1) >= sma_long.shift(1))
rsi_buy = rsi < rsi_oversold
rsi_sell = rsi > rsi_overbought
macd_up = macd > macd_signal
macd_down = macd < macd_signal
volume_up = df["volume"] > volume_ma * volume_mult
# === 组合条件 ===
raw_buy = ma_golden | rsi_buy
raw_sell = ma_death | rsi_sell
if use_macd:
raw_buy = raw_buy & macd_up
raw_sell = raw_sell | macd_down
if use_volume:
raw_buy = raw_buy & volume_up
# === 四路边缘触发执行信号 ===
def edge(signal):
signal = signal.fillna(False).astype(bool)
return signal & ~signal.shift(1).fillna(False)
open_long = edge(raw_buy)
open_short = edge(raw_sell)
df["open_long"] = open_long
df["close_short"] = open_long
df["open_short"] = open_short
df["close_long"] = open_short
# === 买卖标记点 ===
buy_marks = [df["low"].iloc[i] * 0.995 if df["open_long"].iloc[i] else None for i in range(len(df))]
sell_marks = [df["high"].iloc[i] * 1.005 if df["open_short"].iloc[i] else None for i in range(len(df))]
# === 图表输出配置 ===
output = {
"name": my_indicator_name,
"plots": [
{"name": f"SMA{sma_short_period}", "data": sma_short.fillna(0).tolist(), "color": "#FF9800", "overlay": True},
{"name": f"SMA{sma_long_period}", "data": sma_long.fillna(0).tolist(), "color": "#3F51B5", "overlay": True},
{"name": "RSI", "data": rsi.fillna(50).tolist(), "color": "#722ED1", "overlay": False},
{"name": "MACD", "data": macd.fillna(0).tolist(), "color": "#13C2C2", "overlay": False}
],
"signals": [
{"type": "buy", "text": "B", "data": buy_marks, "color": "#00E676"},
{"type": "sell", "text": "S", "data": sell_marks, "color": "#FF5252"}
]
}