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

15 KiB
Raw Permalink Blame History

QuantDinger 脚本代码开发指南

本文说明如何编写可回测、可实盘、可维护的 QuantDinger 脚本代码。脚本代码适合有运行状态的策略,例如分层建仓、篮子均价止盈、追踪止损、金字塔加仓、冷却期、重复下单防护等。

1. 核心边界

脚本代码只负责策略逻辑。用户运行时填写的内容不应该再写进代码参数。

运行面板负责:

  • 标的,例如 BTC/USDT
  • 市场类型,现货或合约
  • 交易方向,做多、做空或双向
  • 投入金额
  • 合约杠杆
  • 账户、通知、实盘风控开关

脚本代码负责:

  • 入场条件
  • 加仓、减仓、止盈、止损规则
  • 分层数量、间距、倍数、周期、冷却期
  • 状态持久化和防重复下单
  • 日志与篮子状态 checkpoint

不要在脚本中写:

ctx.param('direction', 'long')
ctx.param('market_type', 'swap')
ctx.param('investment_amount', 1000)
ctx.param('leverage', 3)
ctx.param('base_notional', 50)

这些值应由运行面板传入,可在脚本中读取:

direction = ctx.direction
market_type = ctx.market_type
budget = ctx.investment_amount
leverage = ctx.leverage

2. 生命周期

脚本必须包含:

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 支持:

bar['open']
bar['high']
bar['low']
bar['close']
bar['volume']
bar['timestamp']

3. 参数设计

只把策略结构参数放进 ctx.param(...)

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 金额。回测和实盘会根据市场类型、价格和杠杆换算为实际下单数量。

示例:

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. 方向处理

方向由运行面板选择。脚本只读取,不写死。

推荐写法:

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

示例:

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) 适合所有有篮子概念的策略。

常用方法:

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 用于展示策略当前状态:

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. 沙箱限制

脚本在安全沙箱中执行。不要使用:

  • getattrsetattrdelattr
  • evalexecopencompile
  • globalsvarsdir
  • __builtins__、dunder 属性
  • 文件、网络、数据库、进程、线程相关 API
  • ossysrequestsurllibsocketsubprocessthreadingmultiprocessingsqlite3psycopgsqlalchemypathlibtempfileglobiooperatorpicklectypes

读取可选字段时,用 try/except 直接访问:

try:
    direction = str(ctx.direction)
except Exception:
    direction = 'long'

不要写:

direction = getattr(ctx, 'direction', 'long')

10. 推荐模板类型

经典可落地的脚本模板包括:

  • EMA ATR 趋势风控:趋势跟随、ATR 止损、ATR 追踪止损。
  • Donchian 突破金字塔:通道突破入场,盈利后顺势加仓。
  • 布林均值回归篮子:触及布林外轨后分层建仓,均价回归止盈。
  • 阶梯分仓马丁篮子:多层分仓、子单马丁、均价止盈、硬止损。

这些模板覆盖趋势、突破、震荡均值回归和高风险篮子模型,避免把多个相似的“加仓模板”重复展示给用户。

11. 复杂策略的推荐架构

复杂脚本不要写成一大坨 if/else。推荐拆成五层:

  1. 信号层:判断是否允许入场、是否允许加仓、是否应该退出。
  2. 资金层:把运行面板的投入金额拆成每一层、每一单的计划金额。
  3. 状态层:保存当前阶段、层数、子单、均价、触发价、冷却期。
  4. 执行层:只负责发出 basket.open_child_orderbasket.close_all
  5. 风控层:硬止损、最大层数、最大订单数、冷却期、同 K 线防重复。

推荐文件结构虽然仍然是一个脚本,但代码内部应保持这种顺序:

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 平仓后冷却 不允许重新入场

脚本里可以不用显式保存字符串状态,但逻辑上必须有这套概念。最少要保存:

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)

主流程建议固定为:

def on_bar(ctx, bar):
    # 1. 数据不足直接返回
    # 2. 读取运行面板方向、价格、状态
    # 3. 如果空仓,检查是否需要重置状态
    # 4. 如果冷却中,直接返回
    # 5. 如果空仓,判断入场
    # 6. 如果持仓,先判断止盈/止损
    # 7. 如果仍持仓,再判断加仓
    # 8. 发单后更新 checkpoint 和 state

不要先加仓再判断止损。复杂策略里这个顺序很重要。

13. 资金拆分模型

用户只填一个“投入金额”。脚本需要根据策略结构拆分每个子单。

13.1 等额分层

适合突破金字塔:

base_notional = ctx.investment_amount / ctx.max_layers

如果投入 1000 USDT、4 层,则每层 250 USDT。

13.2 温和递增分层

适合布林均值回归:

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 单:

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. 信号层设计

复杂策略的入场信号最好是“事件”,不是“状态”。

不推荐:

if not has_position and fast > slow:
    open_position()

这样会在趋势状态持续时频繁尝试入场,必须依赖其他防线兜底。

推荐:

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. 时间风控:冷却期、超时退出、长时间未成交后的状态重置。

示例:

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 生成复杂脚本时,提示词要明确边界。推荐这样写:

写一个 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、文件、网络、数据库、导入危险模块。

如果要改现有模板,建议说:

基于当前模板,只调整参数和风控,不要改变运行面板边界。
把止盈改为 0.8%,层间距改成逐层扩大,最大亏损 12% 硬止损。

19. 最小骨架

"""
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.