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

Strategy API V2 Development Guide

Applies to: the current executable QuantDinger strategy contract Audience: first-time strategy authors, indicator-conversion users, and developers targeting both backtest and live execution

QuantDinger has one current executable Python strategy contract: Strategy API V2. The same source compiles into a strategy manifest used by backtest and live runtimes for instruments, subscriptions, events, order intents, portfolio accounting, and protection rules.

The source owns its market, instruments, frequency, schedules, and trading logic. Run forms provide dates, initial capital, costs, source-permitted leverage, and user parameters; they do not override source-controlled markets, symbols, or timeframes.

Chart indicators are separate artifacts. Their plots, signals, and layers cannot place orders. Convert an indicator into Strategy API V2 before backtesting or deploying it.


1. Quick start: a minimal executable strategy

"""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",
        )

Workflow:

  1. Create a script in the Strategy IDE and paste the source.
  2. Save the source.
  3. Verify it and inspect the compiled manifest.
  4. Choose dates, capital, commission, slippage, and parameters.
  5. Inspect executions, closed trades, the order ledger, equity, and holdings.
  6. Create a deployment only after the backtest behaves as intended. New deployments start stopped.

2. Compiler requirements and authoring standard

Hard compiler requirements:

  • Source is non-empty and executes in the safe sandbox.
  • initialize(context) exists.
  • initialize declares a static universe, index, or named pool through context.set_universe(...).
  • If no subscription is declared, the compiler adds a default daily subscription; this guide still recommends an explicit context.subscribe.
  • The source exposes handle_data, on_rebalance, or at least one registered schedule callback.
  • Leveraged strategies satisfy the Crypto-swap-only policy.

The project authoring standard additionally requires:

  • Start with a triple-quoted docstring. Its first line is the strategy name; following lines describe universe, signals, schedule, and risk.
  • Use English identifiers and source comments.
  • Use stable, auditable parameter and reason names.
  • Avoid look-ahead, implicit reversals, unbounded scaling, and uncapped exposure.

initialize runs during compilation/manifest discovery. Use it for declarations and initial g state. Do not request market data, inspect real positions, or place orders there.


3. The source-owned manifest

Compilation discovers:

  • API version and source hash;
  • CTA or portfolio classification;
  • static or dynamic universe;
  • subscribed instruments, frequency, and fields;
  • schedules;
  • benchmark;
  • lifecycle handlers;
  • factor and fundamental dependencies;
  • warm-up bars;
  • leverage permission and maximum;
  • custom metadata.

Verification endpoint:

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

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

A valid response contains valid: true and the manifest. Verify the final saved source before deployment, not only an earlier draft.


4. Canonical instruments

Market Example
China A-share CNStock:600519.SH
US equity USStock:MSFT
Hong Kong equity HKStock:00700.HK
Crypto spot Crypto:BTC/USDT@spot
Venue-specific Crypto spot Crypto:BTC/USDT@okx:spot
Crypto perpetual Crypto:BTC/USDT@swap
Venue-specific perpetual Crypto:BTC/USDT@okx:swap

The parser also normalizes selected aliases, such as 600519.XSHG to CNStock:600519.SH and BTCUSDT to BTC/USDT.

Production strategies should use the full market prefix. Crypto defaults to spot when no market type is present. Only swap instruments can permit contract leverage.


5. Static and dynamic universes

Static single instrument:

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

Static basket:

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

Index universe:

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

Named platform pool:

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

Dynamic universes resolve point-in-time constituents. Do not copy today's pool members into source and then use them for a historical backtest.

A dynamic universe, more than one static instrument, or on_rebalance normally classifies the manifest as portfolio. One static instrument normally classifies it as CTA.


6. Subscriptions, warm-up, and benchmark

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

Rules:

  • Frequency belongs in source, for example 1m, 5m, 1h, 4h, 1d, or 1w.
  • Aliases such as daily, day, and d normalize to 1d.
  • Omitting symbols subscribes the current universe.
  • set_warmup asks the data service for history before the requested backtest start. It does not remove the need for len(bars) guards.
  • A benchmark is for comparison; it is not traded automatically.
  • The get_history frequency argument is API-compatible metadata. The current runtime reads subscribed frames, so request the same frequency the source subscribes.

7. Lifecycle and schedules

Supported handlers:

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

Schedule registration:

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

Rules:

  • weekday is 1–7, with Monday as 1.
  • A monthday past the end of a month resolves to that month's last day.
  • On daily or lower-frequency bars, a specific intraday time does not create a nonexistent bar.
  • Prefer callback(context, data); the runtime also adapts callbacks that accept only context.
  • A portfolio strategy with no registered schedules invokes on_rebalance.
  • The current engine invokes before_trading_start and after_trading_end for every event timestamp. Do not assume they run only once per calendar day in an intraday strategy.

8. The critical timing model

Backtests expose only point-in-time-visible data:

  1. At a new bar, orders queued after the previous close execute first, using the current open.
  2. before_trading_start and due schedule callbacks see data only through the previous bar; their orders can be processed at the current open.
  3. The current completed bar becomes visible and handle_data runs.
  4. Orders emitted by handle_data wait for the next bar open.
  5. after_trading_end also sees the current bar; its new orders wait for the next bar.

This implements “confirm on close, fill at next open” without future leakage. Never use negative shifts or future rows to move execution earlier.

Live sessions process each closed bar once and preserve g state. Receiving the same bar twice should not duplicate strategy work.


9. context, data, and g

Common context fields:

Field Meaning
context.params run parameters
context.current_dt current event timestamp
context.previous_trading_date previous event timestamp
context.portfolio.starting_cash initial capital
context.portfolio.available_cash available cash
context.portfolio.total_value current equity
context.portfolio.positions current position map
context.data data view

Use data.current(symbol, field) for a current visible value, data.history(symbols, count, fields) for history, and data[symbol] for its current visible DataFrame.

Persist state across callbacks on g:

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

Do not store strategy state in files, databases, or external module services. g is the per-run user state namespace.


10. Parameters

# @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

Read values through context:

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))

Declared defaults and code fallbacks must agree. The parameter panel supplies context.params; the fallback remains the final default when a value is absent.

Symbols, market, timeframe, and leverage permission are source contract fields. Do not disguise them as ordinary run-form overrides.


11. History, factors, and fundamentals

Single-instrument history:

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

One instrument returns a DataFrame. Multiple instruments return a dict of canonical instrument keys to DataFrames:

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

Technical indicators and factors:

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:

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

Other public aliases include REVENUE_GROWTH, DEBT_TO_EQUITY, and FREE_CASH_FLOW. Use only real point-in-time fields supported by the platform; do not invent fields or read future reports.

Pass a symbol to factor/indicator in a multi-asset strategy. The symbol may be omitted only when the data portal has exactly one instrument.


12. Positions and order APIs

Read positions:

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

Common Position fields:

  • symbol
  • amount
  • avg_cost
  • last_price
  • market_value

Order functions:

Function Meaning
order(symbol, amount) add/subtract a quantity
order_value(symbol, value) add/subtract quote-currency value
order_target(symbol, amount) set a target quantity
order_target_value(symbol, value) set a target quote value
order_target_percent(symbol, percent) set a target share of portfolio equity

Target APIs are usually best for repeatable rebalancing. Give every order a stable reason:

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

Write spot and all non-Crypto markets as long-only under the current product policy. A long exit and a short entry are independent; do not turn a zero target into a negative position automatically.

The engine accounts for commission, slippage, lot size, liquidity caps, price limits, and suspensions. Deferred and rejected requests appear in the order audit ledger. “No fill” does not necessarily mean “no signal.”


13. Stop, take-profit, trailing, and time protection

Attach protection to an entry:

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,
)

Or set defaults for later entries:

set_default_protection(
    stop_loss_pct=0.03,
    take_profit_pct=0.08,
)

Percentage fields are ratios: 0.03 means 3%. Values are clamped to safe ranges, and negatives become zero.

Backtest behavior:

  • A gap through a protection threshold fills at the available bar open.
  • An intrabar touch fills at the trigger price.
  • If several protections trigger in one bar, conservative mode prioritizes stop-loss, trailing stop, time limit, then take-profit.

Live execution checks the same protection semantics on an independent price clock instead of waiting for the next strategy bar. Protection state can be persisted and restored after a session restart.


14. Leverage and shorting

Only a static universe consisting entirely of Crypto swap instruments may declare:

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)

Rules:

  • Do not call allow_leverage for Crypto spot, equities, index/pool universes, or non-Crypto markets.
  • Dynamic universes cannot enable contract leverage.
  • Backtest/deployment leverage cannot exceed the source maximum.
  • A run form cannot force leverage on when the source has not permitted it.
  • The runtime applies the selected leverage; do not multiply order sizing by leverage again.
  • Shorting belongs only in swap strategies and requires independent short-entry, short-exit, and risk rules.

15. Complete CTA tutorial: dual EMA trend

"""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",
        )

Why it is structured this way:

  • Universe, frequency, and benchmark live in source.
  • Warm-up covers the slow EMA, while runtime length is still checked.
  • Invalid fast/slow combinations stop the current event.
  • Entry and exit are exclusive; the bearish condition exits a long but does not short.
  • A completed daily bar emits an order for the next open.
  • Protection is attached to the entry; the exit targets zero.

16. Portfolio tutorial: weekly factor rebalance

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

This strategy class must use point-in-time universe and factor data. Evaluate coverage, survivorship bias, turnover, trading costs, lot sizes, and unfilled orders in addition to headline return.


17. Backtests, results, and diagnosis

Core backtest request:

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

You may supply sourceId or strategyId to load saved source. The request cannot override source markets, instruments, or frequency.

Inspect:

  • resultStatus: no_signals, open_position_only, or completed_trades.
  • totalExecutions: fill count.
  • totalTrades: closed round-trip count, not fill count.
  • rawTrades/executions: opens, adds, reductions, and closes.
  • closedTrades: completed round trips.
  • orderLedger: fills, deferrals, rejections, and reasons.
  • holdingSnapshots/rebalanceRecords: portfolio evolution.
  • equityCurve, drawdown, win rate, Profit Factor, benchmark, and excess return.
  • dataProvenance/executionAssumptions: data origin and fill model.

Zero executions can be valid: insufficient history, conditions never met, poor parameters, missing data, or rejected orders. Read logs and the order ledger before treating it as an engine failure.


18. Deployment and live boundaries

Core deployment fields include:

  • sourceId
  • name
  • initialCapital
  • executionMode: signal or live
  • optional credentialId, params, leverage, position side, and notifications

A new deployment is stopped and must be started explicitly. Stop it before deletion.

Current live-account boundaries:

  • Crypto live execution requires a supported exchange credential.
  • USStock live execution requires Alpaca or IBKR.
  • Mixed-market live deployment is unsupported.
  • Other markets cannot be forced through a mismatched credential.

Use signal mode first to validate notifications, signal frequency, and state restoration. A successful backtest does not prove that credentials, balances, venue rules, minimum order sizes, and network health are ready for live trading.


19. Sandbox and common failures

Strategy source runs in a safe execution environment. File, network, database, process, dynamic execution, reflection, and unsafe imports are prohibited. Do not use eval, exec, compile, open, dunder bypasses, or external state.

Error Meaning Fix
strategyV2.codeRequired empty source submit complete source
strategyV2.initializeRequired initialize missing add it
strategyV2.initializeFailed:... initialization failed keep initialize declarative
strategyV2.universeRequired universe missing call set_universe
strategyV2.handlerRequired no handler/schedule add a handler or schedule
strategyV2.leverageCryptoSwapOnly invalid leverage market use static Crypto swaps only
strategyV2.leverageNotAllowed run requests unpermitted leverage permit it legally or disable it
strategyV2.leverageExceedsStrategyLimit requested leverage too high lower the request
strategyV2.dataUnavailable:... instrument data unavailable check canonical symbol and range
strategyV2.runtimeFailed:... handler raised inspect the named handler and cause

20. Pre-publication checklist

  • [ ] The file has an English docstring covering name, universe, signals, schedule, and risk.
  • [ ] initialize only declares universe, subscription, warm-up, benchmark, schedules, leverage permission, and initial g.
  • [ ] Instruments are canonical and Crypto explicitly distinguishes spot/swap.
  • [ ] The source owns instruments and frequency; no run-form override is assumed.
  • [ ] Parameter defaults and code fallbacks agree.
  • [ ] Every history window checks actual length.
  • [ ] No future rows, negative shifts, or centered rolling.
  • [ ] Long exits and short entries are independent.
  • [ ] Exposure is capped; grid, DCA, martingale, and scaling layers have hard limits.
  • [ ] Every order has an auditable reason.
  • [ ] Risk percentages use decimal ratios.
  • [ ] Leverage is declared only for Crypto swaps and is not multiplied twice.
  • [ ] The manifest verifies successfully.
  • [ ] The order ledger is reviewed, not only the equity curve.
  • [ ] Robustness is tested across periods and cost assumptions.
  • [ ] At least one successful backtest exists before publication.
  • [ ] Credentials, market, balance, lot size, and notifications are checked before live use.