QuantDingerQuantDingerv5.0.1 Docs 文档中心
中 / EN GitHub

Strategy API V2 策略开发指南

适用范围:当前 QuantDinger 可执行策略契约 Strategy API V2 面向读者:第一次编写策略的用户、指标转策略用户,以及需要同时覆盖回测与实盘的策略开发者

QuantDinger 只有一套当前可执行的 Python 策略契约:Strategy API V2。同一份源码会编译成策略清单,并由回测和实盘运行时共享标的、订阅、事件模型、订单意图、组合记账和保护规则。

策略源码拥有市场、标的、周期、调度和交易逻辑。运行面板只提供日期、初始资金、交易成本、源码允许范围内的杠杆,以及用户参数;它不能改写源码声明的市场、标的或周期。

图表指标是另一种产物。指标中的 plots、signals 和 layers 不能下单,必须先转换成 Strategy API V2。


1. 快速开始:最小可运行策略

"""SPY 20-Day Moving Average
Trades a long-only SPY regime from completed daily bars.
"""

# @param period int 20 Moving-average period range=5:100:5
# @param target_pct float 0.95 Target portfolio weight range=0.1:1.0:0.05


def initialize(context):
    g.symbol = "USStock:SPY"
    context.set_universe([g.symbol])
    context.subscribe(
        frequency="1d",
        fields=["open", "high", "low", "close", "volume"],
    )
    context.set_warmup(120)
    context.set_benchmark("USStock:SPY")


def handle_data(context, data):
    period = int(context.params.get("period", 20))
    target_pct = float(context.params.get("target_pct", 0.95))

    bars = get_history(
        period + 1,
        "1d",
        "close",
        g.symbol,
    )
    if len(bars) < period:
        return

    price = float(bars["close"].iloc[-1])
    average = float(bars["close"].tail(period).mean())
    position = get_position(g.symbol)
    desired = target_pct if price > average else 0.0

    if desired > 0 and position.amount <= 0:
        order_target_percent(
            g.symbol,
            desired,
            reason="ma_long_entry",
            stop_loss_pct=0.05,
        )
    elif desired == 0 and position.amount > 0:
        order_target_percent(
            g.symbol,
            0.0,
            reason="ma_long_exit",
        )

运行步骤:

  1. 在策略 IDE 新建脚本并粘贴源码。
  2. 保存源码。
  3. 调用验证或在界面点击验证,确认编译清单正确。
  4. 选择回测日期、初始资金、手续费、滑点和参数。
  5. 检查成交、已平仓交易、订单审计、权益曲线和持仓快照。
  6. 只有回测符合预期后才创建部署;新部署默认为停止状态。

2. 编译器硬性要求与编写规范

编译器硬性要求:

  • 源码非空且能在安全沙箱中执行。
  • 必须定义 initialize(context)
  • initialize 必须通过 context.set_universe(...) 声明静态标的、指数或命名股票池。
  • 如果未显式订阅,编译器会创建默认日线订阅;教程仍建议始终显式调用 context.subscribe
  • 必须存在 handle_dataon_rebalance,或至少注册一个定时回调。
  • 杠杆策略必须满足 Crypto swap 专用规则。

项目编写规范还要求:

  • 文件以三引号 docstring 开头;第一行是策略名称,后续说明标的、信号、调度和风控。
  • 标识符和源码注释使用英文。
  • 参数和交易原因使用稳定、可审计的名称。
  • 禁止未来数据、隐式反手、无界加仓和不受控仓位。

initialize 在编译/清单发现阶段执行,用于声明配置和初始化 g。不要在这里请求行情、读取真实仓位或下单。


3. 源码拥有的策略清单

编译后清单包含:

  • API 版本与源码哈希;
  • CTA 或 portfolio 类型;
  • 静态/动态 universe;
  • 订阅标的、周期和字段;
  • 定时任务;
  • benchmark;
  • 生命周期处理器;
  • 因子和基本面依赖;
  • warm-up 数量;
  • 是否允许杠杆及最大杠杆;
  • 自定义 metadata。

验证接口:

POST /api/strategies/verify
Content-Type: application/json

{"code": "...complete Strategy API V2 source..."}

成功响应会返回 valid: true 和 manifest。部署前必须重新验证最终保存的源码,不要只验证早期草稿。


4. 标的规范

推荐使用规范标的:

市场 示例
A 股 CNStock:600519.SH
美股 USStock:MSFT
港股 HKStock:00700.HK
Crypto 现货 Crypto:BTC/USDT@spot
指定交易所 Crypto 现货 Crypto:BTC/USDT@okx:spot
Crypto 永续 Crypto:BTC/USDT@swap
指定交易所 Crypto 永续 Crypto:BTC/USDT@okx:swap

系统也会规范化部分别名,例如 600519.XSHGCNStock:600519.SHBTCUSDTBTC/USDT

为避免歧义,生产策略应写完整市场前缀。Crypto 未写市场类型时默认为 spot。只有 swap 可以启用合约杠杆。


5. 静态和动态 universe

静态单标的:

context.set_universe(["USStock:SPY"])

静态多标的:

context.set_universe([
    "USStock:AAPL",
    "USStock:MSFT",
    "USStock:NVDA",
])

指数 universe:

context.set_universe(index="INDEX:SP500")
members = get_index_stocks("INDEX:SP500")

平台命名股票池:

context.set_universe(pool="sp500")
members = get_universe_stocks()

动态 universe 在每个历史时点解析当时成分,避免直接把今天的成分复制进历史回测。不要把 pool 成分硬编码进源码。

使用动态 universe、多个静态标的或 on_rebalance 时,清单通常分类为 portfolio;单一静态标的通常分类为 CTA。


6. 订阅、预热和 benchmark

context.subscribe(
    frequency="1d",
    fields=["open", "high", "low", "close", "volume"],
)
context.set_warmup(260)
context.set_benchmark("USStock:SPY")

要点:

  • 周期写在源码中,例如 1m5m1h4h1d1w
  • dailydayd 等别名会规范化为 1d
  • 未指定 symbols 时,订阅当前 universe。
  • set_warmup 告诉数据服务在回测开始日前额外获取历史数据;它不代表策略可以跳过 len(bars) 检查。
  • benchmark 只用于对比收益,不会自动交易。
  • get_history 的 frequency 参数用于 API 兼容;当前运行时从已订阅数据取历史,因此调用周期应与订阅周期保持一致。

7. 生命周期与调度

支持的处理器:

def initialize(context):
    pass

def before_trading_start(context, data):
    pass

def handle_data(context, data):
    pass

def on_rebalance(context, panel):
    pass

def after_trading_end(context, data):
    pass

定时任务:

def initialize(context):
    context.set_universe(["USStock:SPY"])
    context.subscribe(frequency="5m")
    run_daily(rebalance, time="09:35")
    run_weekly(weekly_review, weekday=1, time="09:40")
    run_monthly(monthly_rebalance, monthday=1, time="09:45")

规则:

  • weekday 使用 1–7,1 为星期一。
  • 月度日期超出当月天数时会落在当月最后一天。
  • 日线及更低频率下,具体 time 不用于制造不存在的盘中 bar。
  • 回调推荐签名为 callback(context, data);运行时也会适配只接收 context 的函数。
  • portfolio 策略如果没有定时任务,会调用 on_rebalance
  • 当前引擎在每个事件时间戳调用 before_trading_startafter_trading_end;不要假设它们在分钟策略中每天只调用一次。

8. 最重要的时间语义

回测只向策略暴露当时可见的数据:

  1. 进入新 bar 时,先执行上一 bar 收盘后排队的订单,成交参考当前 bar 开盘。
  2. before_trading_start 和到期的定时回调只看到前一根及更早的数据;其订单可以在当前开盘处理。
  3. 然后当前 bar 变为可见,调用 handle_data
  4. handle_data 根据当前已完成 bar 产生的订单排队到下一根 bar 开盘。
  5. after_trading_end 同样能看到当前 bar;其新订单也等待下一根 bar。

因此,“收盘确认、下一开盘成交”是默认的无未来执行模型。不要用负 shift 或未来行把成交提前。

实盘会对每根已收盘 bar 只处理一次,并保留 g 状态。重复收到同一根 bar 不应重复触发策略。


9. context、data 和 g

常用 context 字段:

字段 含义
context.params 本次运行参数
context.current_dt 当前事件时间
context.previous_trading_date 上一个事件时间
context.portfolio.starting_cash 初始资金
context.portfolio.available_cash 可用现金
context.portfolio.total_value 当前总权益
context.portfolio.positions 当前持仓字典
context.data 数据视图

data.current(symbol, field) 读取当前可见值;data.history(symbols, count, fields) 读取历史;data[symbol] 返回当前可见 DataFrame。

跨回调状态放在 g

def initialize(context):
    g.last_signal = ""
    g.rebalance_count = 0

不要把用户状态放在文件、数据库或模块外部全局服务中。g 是单次运行的策略状态空间。


10. 参数

# @param fast_period int 20 Fast moving-average period range=2:100:1
# @param slow_period int 50 Slow moving-average period range=3:250:1
# @param target_pct float 0.95 Target weight values=0.5,0.75,0.95
# @param enabled bool true Enable entries

读取:

fast_period = int(context.params.get("fast_period", 20))
slow_period = int(context.params.get("slow_period", 50))
target_pct = float(context.params.get("target_pct", 0.95))
enabled = bool(context.params.get("enabled", True))

声明默认值和代码回退值必须一致。参数面板把用户值放入 context.params;若没有用户值,代码回退值是最后保障。

标的、市场、周期和杠杆许可属于源码契约,不要把它们伪装成可由运行面板任意覆盖的普通参数。


11. 历史数据、因子和基本面

单标的历史:

bars = get_history(
    60,
    "1d",
    ["open", "high", "low", "close", "volume"],
    "USStock:SPY",
)

一个标的返回 DataFrame;多个标的返回以规范标的为键的 DataFrame 字典:

frames = data.history(
    ["USStock:AAPL", "USStock:MSFT"],
    count=30,
    fields=["close", "volume"],
)

技术指标和因子:

rsi_value = factor("rsi", g.symbol, period=14)
macd = indicator("MACD", g.symbol, fastperiod=12, slowperiod=26, signalperiod=9)
scores = get_factors(symbols, ["momentum_20", "volatility_20"])

基本面:

fundamentals = get_fundamentals(
    ["PE", "PB", "ROE", "MARKET_CAP"],
    symbols,
)

常用公开别名还包括 REVENUE_GROWTHDEBT_TO_EQUITYFREE_CASH_FLOW。只使用平台真实支持、按时点可见的字段,不要发明字段或读取未来财报。

多标的 factor/indicator 调用必须传 symbol;只有单标的数据门户可以省略 symbol。


12. 仓位与订单 API

读取仓位:

position = get_position(g.symbol)
all_positions = get_positions()

Position 常用字段:

  • symbol
  • amount
  • avg_cost
  • last_price
  • market_value

订单函数:

函数 含义
order(symbol, amount) 增减指定数量
order_value(symbol, value) 增减指定报价币价值
order_target(symbol, amount) 把持仓调整到目标数量
order_target_value(symbol, value) 调整到目标价值
order_target_percent(symbol, percent) 调整到组合权益的目标比例

目标型 API 最适合可重复执行的再平衡逻辑。每个订单都应提供稳定的 reason

order_target_percent(
    g.symbol,
    0.5,
    reason="breakout_long_entry",
)

现货和所有非 Crypto 市场当前按 long-only 编写。多头离场条件与空头入场条件必须独立;不要把 target=0 的离场自动改成负仓位。

引擎会处理手续费、滑点、最小交易单位、成交量上限、涨跌停和停牌。被延迟或拒绝的订单会出现在订单审计账本中,不应从“没有成交”直接推断策略没有发单。


13. 止损、止盈、追踪和时间保护

随开仓声明:

order_target_percent(
    g.symbol,
    0.8,
    reason="breakout_long_entry",
    stop_loss_pct=0.03,
    take_profit_pct=0.08,
    trailing_stop_pct=0.025,
    trailing_activation_pct=0.02,
    time_limit_seconds=86400 * 10,
)

或设置后续开仓的默认保护:

set_default_protection(
    stop_loss_pct=0.03,
    take_profit_pct=0.08,
)

所有 pct 都使用小数比率,0.03 表示 3%。保护值会限制在安全范围内;负值按 0 处理。

回测规则:

  • 跳空越过保护价时按可成交的 bar 开盘价处理。
  • bar 内触发按触发价处理。
  • 同一 bar 同时触发多个保护时,默认 conservative 模式优先止损,再追踪止损、时间限制、止盈。

实盘使用独立价格时钟检查同样的保护语义,不必等待下一根策略 bar。保护状态会保存并可在会话重启后恢复。


14. 杠杆和做空

只有 universe 中全部静态标的都是 Crypto swap 时,源码才能声明:

def initialize(context):
    g.symbol = "Crypto:BTC/USDT@okx:swap"
    context.set_universe([g.symbol])
    context.subscribe(frequency="1h")
    context.allow_leverage(max_leverage=5)

规则:

  • Crypto spot、股票、指数/股票池和其他非 Crypto 市场不能调用 allow_leverage
  • 动态 universe 不能启用合约杠杆。
  • 回测或部署选择的杠杆不能超过源码声明的最大值。
  • 源码没有许可时,运行面板不能强制开启杠杆。
  • 用户选择的杠杆由运行时应用,不要再在订单金额中手工乘一次。
  • 做空只应出现在 swap 策略中,并且必须有独立的空头入场、空头离场和风险规则。

15. 完整 CTA 教程:双 EMA 趋势策略

"""Dual EMA Long Trend
Trades a long-only daily SPY trend with a protected entry and next-open fills.
"""

# @param fast_period int 20 Fast EMA period range=5:80:5
# @param slow_period int 50 Slow EMA period range=20:250:10
# @param target_pct float 0.95 Target portfolio weight range=0.1:1.0:0.05
# @param stop_loss_pct float 0.05 Entry stop-loss ratio range=0.01:0.15:0.01


def initialize(context):
    g.symbol = "USStock:SPY"
    context.set_universe([g.symbol])
    context.subscribe(frequency="1d")
    context.set_warmup(300)
    context.set_benchmark("USStock:SPY")


def handle_data(context, data):
    fast_period = int(context.params.get("fast_period", 20))
    slow_period = int(context.params.get("slow_period", 50))
    target_pct = float(context.params.get("target_pct", 0.95))
    stop_loss_pct = float(context.params.get("stop_loss_pct", 0.05))

    if fast_period >= slow_period:
        log.warning("fast_period must be smaller than slow_period")
        return

    bars = get_history(
        slow_period + 2,
        "1d",
        "close",
        g.symbol,
    )
    if len(bars) < slow_period + 1:
        return

    close = bars["close"]
    fast_now = float(close.ewm(span=fast_period, adjust=False).mean().iloc[-1])
    slow_now = float(close.ewm(span=slow_period, adjust=False).mean().iloc[-1])
    position = get_position(g.symbol)

    if fast_now > slow_now and position.amount <= 0:
        order_target_percent(
            g.symbol,
            target_pct,
            reason="dual_ema_long_entry",
            stop_loss_pct=stop_loss_pct,
        )
    elif fast_now < slow_now and position.amount > 0:
        order_target_percent(
            g.symbol,
            0.0,
            reason="dual_ema_long_exit",
        )

为什么这样写:

  • universe、周期和 benchmark 都在源码中。
  • warm-up 覆盖慢 EMA,但仍检查实际数据长度。
  • 快慢周期错误时直接停止本 bar。
  • 入场与离场互斥,死叉只平多,不开空。
  • 读取当前已完成日线后发单,下一根开盘成交。
  • 只有入场附带保护,离场目标为 0。

16. Portfolio 教程:每周因子再平衡

"""S&P 500 Momentum Basket
Selects the strongest five point-in-time pool members and rebalances weekly.
"""

# @param holdings int 5 Number of holdings range=3:20:1
# @param max_weight float 0.18 Maximum weight per holding range=0.05:0.3:0.01


def initialize(context):
    context.set_universe(pool="sp500")
    context.subscribe(frequency="1d")
    context.set_warmup(80)
    context.set_benchmark("USStock:SPY")
    run_weekly(rebalance, weekday=1, time="09:35")


def rebalance(context, data):
    holdings = int(context.params.get("holdings", 5))
    max_weight = float(context.params.get("max_weight", 0.18))
    symbols = get_universe_stocks()
    if len(symbols) < holdings:
        return

    scores = get_factors(symbols, "momentum_20")
    if scores.empty or "momentum_20" not in scores.columns:
        return

    ranked = scores["momentum_20"].dropna().sort_values(ascending=False)
    selected = list(ranked.head(holdings).index)
    if not selected:
        return

    target_weight = min(max_weight, 0.95 / len(selected))
    current = get_positions()

    for symbol in current:
        if symbol not in selected:
            order_target_percent(symbol, 0.0, reason="weekly_remove")

    for symbol in selected:
        order_target_percent(symbol, target_weight, reason="weekly_select")

此类策略必须使用按时点解析的 universe 和因子数据。回测还要关注覆盖率、幸存者偏差、换手、交易成本、最小交易单位和无法成交订单。


17. 回测、结果和诊断

回测请求的核心字段:

{
  "code": "...",
  "startDate": "2024-01-01",
  "endDate": "2025-12-31",
  "initialCapital": 100000,
  "commission": 0.0005,
  "slippage": 0.0005,
  "leverageEnabled": false,
  "leverage": 1,
  "params": {},
  "persist": true
}

还可以传 sourceIdstrategyId 读取已保存源码。市场、标的和周期不能从请求覆盖。

重点检查:

  • resultStatusno_signalsopen_position_onlycompleted_trades
  • totalExecutions:实际成交次数。
  • totalTrades:已平仓交易次数,不等于成交次数。
  • rawTrades/executions:开仓、加仓、减仓、平仓成交。
  • closedTrades:完整往返交易。
  • orderLedger:成交、延迟、拒绝及原因。
  • holdingSnapshotsrebalanceRecords:组合过程。
  • equityCurve、回撤、胜率、Profit Factor 和 benchmark/excess return。
  • dataProvenanceexecutionAssumptions:数据来源与执行假设。

零成交不一定是系统错误:可能是数据不足、条件从未触发、参数不合理、标的无数据或订单被拒绝。先看日志和 orderLedger。


18. 部署与实盘边界

部署核心字段包括:

  • sourceId
  • name
  • initialCapital
  • executionModesignallive
  • 可选 credentialIdparams、杠杆、仓位方向和通知配置

部署创建后状态为 stopped,必须显式 start。删除前必须先停止。

当前 live 账户边界:

  • Crypto live 需要受支持交易所凭证。
  • USStock live 需要 Alpaca 或 IBKR 凭证。
  • 混合市场 live 不支持。
  • 其他市场不能强行用不匹配的凭证部署。

先用 signal 模式验证通知、信号频率和状态恢复,再考虑 live。回测通过不代表连接、余额、最小下单量、交易所规则和网络状态一定满足实盘。


19. 安全限制和常见失败

策略运行在安全执行环境中。禁止文件、网络、数据库、进程、动态执行、反射和不安全导入。不要使用 evalexeccompileopen、dunder 绕过或外部状态。

常见编译错误:

错误 含义 修复
strategyV2.codeRequired 源码为空 提交完整源码
strategyV2.initializeRequired 缺少 initialize 添加初始化函数
strategyV2.initializeFailed:... 初始化执行失败 只在 initialize 做声明和状态初始化
strategyV2.universeRequired 未声明 universe 调用 set_universe
strategyV2.handlerRequired 没有可执行处理器/定时任务 添加 handler 或 schedule
strategyV2.leverageCryptoSwapOnly 杠杆市场不合法 仅用于静态 Crypto swap
strategyV2.leverageNotAllowed 面板开了源码未许可的杠杆 源码合法许可或关闭杠杆
strategyV2.leverageExceedsStrategyLimit 请求杠杆超过上限 降低请求值
strategyV2.dataUnavailable:... 标的没有可用数据 检查规范标的和数据范围
strategyV2.runtimeFailed:... 回调运行异常 根据处理器名和原始异常修复

20. 发布前检查清单

  • [ ] 文件有英文 docstring,说明名称、universe、信号、调度和风控。
  • [ ] initialize 只声明 universe、订阅、预热、benchmark、调度、杠杆许可和初始 g
  • [ ] 标的使用规范格式,Crypto 明确 spot/swap。
  • [ ] 源码拥有标的和周期,不依赖运行面板覆盖。
  • [ ] 参数默认值与代码回退值一致。
  • [ ] 所有历史窗口都检查长度。
  • [ ] 不使用未来行、负 shift 或居中 rolling。
  • [ ] 多头离场与空头入场独立。
  • [ ] 仓位有明确上限;网格、DCA、马丁和加仓层数有硬限制。
  • [ ] 订单都有可审计 reason。
  • [ ] 风险百分比使用小数比率。
  • [ ] 只对 Crypto swap 声明杠杆,且不重复乘杠杆。
  • [ ] 已验证 manifest。
  • [ ] 已检查 orderLedger,而不只看收益曲线。
  • [ ] 已用不同时间区间和成本假设做稳健性测试。
  • [ ] 已有至少一次成功回测后再发布。
  • [ ] live 前先确认凭证、市场、余额、最小交易单位和通知。