532 lines
15 KiB
Markdown
532 lines
15 KiB
Markdown
# 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.
|
||
```
|