Files
quantdingerdocs/SCRIPT_CODE_DEVELOPMENT_GUIDE_CN.md
2026-07-11 20:13:58 +00:00

532 lines
15 KiB
Markdown
Raw Permalink 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 脚本代码开发指南
本文说明如何编写可回测、可实盘、可维护的 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.
```