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

16 KiB

QuantDinger Script Code Development Guide

This guide explains how to write QuantDinger script code that can be backtested, deployed live, and maintained safely. Script code is for stateful strategies such as layered entries, basket average-cost take profit, trailing stops, pyramiding, cooldowns, and duplicate-order protection.

1. Core Boundary

Script code owns strategy logic only. Values chosen by the user at run time should not be duplicated as code parameters.

The run panel owns:

  • Symbol, such as BTC/USDT
  • Market type, spot or swap
  • Trade direction, long, short, or both
  • Investment amount
  • Contract leverage
  • Account, notifications, and live risk controls

Script code owns:

  • Entry conditions
  • Add, reduce, take-profit, and stop-loss rules
  • Layer count, spacing, multipliers, periods, and cooldowns
  • Persistent state and duplicate-order protection
  • Logs and basket checkpoints

Do not write these as script parameters:

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

Read run-panel values directly when needed:

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

2. Lifecycle

Every script must define:

def on_init(ctx):
    pass

def on_bar(ctx, bar):
    pass

on_init(ctx) runs once when the script starts. Use it to read strategy parameters and initialize defaults.

on_bar(ctx, bar) runs once per K-line bar. The platform uses a fixed 1m bar stream for script strategies; live execution also checks the latest price every 10 seconds to reduce take-profit and stop-loss latency. Backtests are K-line based, so do not write tick-level strategies that require every market tick.

bar supports:

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

3. Parameter Design

Only put strategy knobs into 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)

Percent defaults in Python code use 0-1 ratios:

  • 0.006 means 0.6%
  • 0.02 means 2%
  • 0.8 means 80%

The frontend may display 0-100 percent values, but the generated Python default literal should remain a ratio.

4. Investment Amount and Order Sizing

For stateful scripts, prefer ctx.basket(side).open_child_order(..., notional=quote_amount).

notional means quote-currency amount, for example USDT. Backtest and live execution convert it into base quantity using market type, price, and leverage.

Example:

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

Do not expose "base order amount" as a template parameter. A clearer model is: the user enters one investment amount, and the script derives child order amounts from layer count, multipliers, and total weights.

5. Direction Handling

Direction is selected in the run panel. The script reads it, but should not hard-code it.

Recommended helper:

def _side(ctx):
    try:
        direction = str(ctx.direction)
    except Exception:
        direction = 'long'
    return 'short' if direction.lower() == 'short' else 'long'

Spot can only run long. That constraint is enforced by the run panel and execution layer; scripts do not need a second complicated branch.

6. State Management

Layered, basket, or cooldown-based strategies must use ctx.state.

Common state keys:

  • Current layer: layer
  • Current child order: order
  • Average cost: avg_cost
  • Total quantity: qty
  • Next trigger price: next_trigger
  • Cooldown-until bar: cooldown_until
  • Last order bar: last_order_bar

Example:

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)

This prevents duplicate orders on the same bar.

7. Basket API

ctx.basket(side) is the preferred API for strategies with basket semantics.

Common usage:

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

Use checkpoint to expose the current strategy state:

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. Backtest and Live Alignment

Script backtest and live execution share the same on_init/on_bar semantics. Keep these points in mind:

  • Backtests are based on historical K-line bars and do not simulate every 10-second live price check.
  • Live execution additionally checks latest price for faster price-condition handling.
  • Use basket notional sizing so leverage behavior stays consistent across backtest and live.
  • Do not use future data. Use ctx.bars(n) for current and historical bars only.
  • Every scale-in needs a price distance, max layer/order limit, and duplicate-order guard.

9. Sandbox Restrictions

Scripts run inside a safety sandbox. Do not use:

  • getattr, setattr, delattr
  • eval, exec, open, compile
  • globals, vars, dir
  • __builtins__ or dunder attributes
  • File, network, database, process, or thread APIs
  • os, sys, requests, urllib, socket, subprocess, threading, multiprocessing, sqlite3, psycopg, sqlalchemy, pathlib, tempfile, glob, io, operator, pickle, or ctypes

For optional fields, use direct access with try/except:

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

Do not write:

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

Professional script templates should cover different market regimes instead of repeating the same scale-in idea:

  • EMA ATR trend risk: trend following with ATR hard stop and ATR trailing stop.
  • Donchian breakout pyramid: channel breakout entry and favorable pyramiding.
  • Bollinger mean reversion basket: layered entries around volatility-band extremes and average-cost reversion exit.
  • Layered basket martingale: multiple layers, martingale child sizing, average-cost take profit, and hard stop.

These four cover trend, breakout, range reversion, and high-risk basket models without showing several duplicate "add-on-dip" templates.

Do not write complex scripts as one large if/else block. Split the logic into five layers:

  1. Signal layer: decide whether entry, add, or exit is allowed.
  2. Sizing layer: split the run-panel investment amount into planned child order amounts.
  3. State layer: persist phase, layer, child order, average cost, trigger price, and cooldown.
  4. Execution layer: emit only basket.open_child_order or basket.close_all.
  5. Risk layer: hard stop, max layers, max child orders, cooldown, and duplicate-order guards.

The script is still a single file, but the internal structure should look like this:

def on_init(ctx):
    # 1. Read strategy knobs
    pass

def _side(ctx):
    # 2. Read run-panel direction
    pass

def _run_budget(ctx):
    # 3. Read run-panel investment amount
    pass

def _planned_notional(ctx, layer, order):
    # 4. Calculate planned quote amount for each child order
    pass

def _entry_signal(ctx, bar, bars):
    # 5. Entry signal
    pass

def _risk_exit(ctx, price, avg_cost):
    # 6. Risk exit
    pass

def _place_child(ctx, basket, layer, order, price, action):
    # 7. Place order and update state
    pass

def on_bar(ctx, bar):
    # 8. Main orchestration only
    pass

This makes it much easier to add conditions, change sizing, or replace exit logic without rewriting the whole strategy.

12. State Machine Model

Complex strategies should start with a state machine design. Recommended phases:

State Meaning Allowed actions
idle Flat and waiting Check entry
opening First order placed or basket building Add, take profit, stop
active Position is running Take profit, stop, trailing exit
closing Close triggered Wait for execution layer
cooldown Post-exit cooldown No re-entry

The script does not have to store these exact strings, but the logic should follow this model. At minimum persist:

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)

Recommended on_bar flow:

def on_bar(ctx, bar):
    # 1. Return if not enough data
    # 2. Read direction, price, and state
    # 3. If flat, repair/reset stale state
    # 4. If in cooldown, return
    # 5. If flat, check entry
    # 6. If in position, check take-profit/stop first
    # 7. If still in position, check add logic
    # 8. After any order, update checkpoint and state

Do not add first and check stop later. In complex scripts, this order matters.

13. Sizing Models

Users enter one investment amount. The script should split it according to the strategy structure.

13.1 Equal Layers

Useful for breakout pyramids:

base_notional = ctx.investment_amount / ctx.max_layers

If investment is 1000 USDT and there are 4 layers, each layer is 250 USDT.

13.2 Mild Geometric Layers

Useful for Bollinger mean reversion:

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

If investment is 1000 USDT, 4 layers, multiplier 1.25, weights are 1, 1.25, 1.56, and 1.95.

13.3 Layered Martingale

Useful for 5 layers and 3 child orders per layer:

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

If investment is 1000 USDT, 5 layers, 3 orders per layer, multiplier 1.8:

  • One layer weight: 1 + 1.8 + 3.24 = 6.04
  • Total weight: 6.04 * 5 = 30.2
  • Base child order amount: 1000 / 30.2 = 33.11 USDT
  • Each layer has roughly 33.11 / 59.60 / 107.27 USDT

This removes the conflict between "base order amount in code" and "investment amount in the run panel".

14. Signal Layer Design

Complex entries should usually be events, not persistent states.

Not recommended:

if not has_position and fast > slow:
    open_position()

That condition remains true for many bars and relies on other guards to avoid repeated entry attempts.

Recommended:

cross_up = prev_fast <= prev_slow and fast > slow
if not has_position and cross_up:
    open_position()

Common signal models:

  • Trend: EMA cross, price crossing and confirming above/below a moving average.
  • Breakout: close breaks the highest high or lowest low of the past N bars.
  • Mean reversion: price touches an outer Bollinger band with RSI confirmation.
  • Layered martingale: first order may start immediately or use a filter; later orders must be triggered by price distance only.

15. Risk Layer Design

Every complex strategy needs at least three types of risk control:

  1. Structure risk: max layers, max child orders, one order per bar.
  2. Price risk: hard stop, average-cost take profit, trailing stop, channel exit.
  3. Time risk: cooldown, timeout exit, stale-state reset.

Example:

if last_order_bar == bar_no:
    return

if layer >= ctx.max_layers and order >= ctx.orders_per_layer:
    # No more adds. Wait for take-profit or hard stop.
    return

if pnl <= -ctx.hard_stop_pct:
    basket.close_all(reason='hard_stop')
    _reset_cycle(ctx, bar_no + ctx.cooldown_bars)
    return

16. Pre-Backtest Checklist

Before running a complex script backtest, check:

  • on_init(ctx) and on_bar(ctx, bar) are defined.
  • No getattr, file, network, database, process, or other sandbox-blocked capability is used.
  • Direction, market type, investment amount, and leverage are not defined as ctx.param.
  • Percent parameters in Python are 0-1 ratios.
  • Every add has max count and price distance.
  • Every order has a same-bar duplicate guard.
  • Order sizing is derived from ctx.investment_amount.
  • Orders use basket.open_child_order(..., notional=...) for quote amount sizing.
  • State is reset after take-profit/stop and cooldown is set.
  • Every state variable can be explained.

17. Pre-Live Checklist

A successful backtest does not mean the script is ready for live trading. Also check:

  • Spot scripts only run long.
  • Contract leverage fits exchange limits.
  • Investment amount can cover the maximum planned layers.
  • Max order count does not violate exchange rate limits or minimum order size.
  • Hard stop exists and is not so wide that it is meaningless.
  • Account-level risk controls, notifications, and emergency stop are configured.
  • The script has completed at least one full open/add/close cycle with small capital or demo trading.

18. AI Assistant Prompt Examples

When asking AI to generate complex script code, state the boundary explicitly:

Write a QuantDinger script code:
1. Use on_init/on_bar.
2. The run panel owns symbol, spot/swap, direction, investment amount, and leverage. Do not create ctx.param for these.
3. Use ctx.basket(side).open_child_order(..., notional=...) for orders.
4. Split the investment amount into 5 layers, 3 child orders per layer, martingale multiplier 1.8.
5. Use child-order spacing, average-cost take profit, hard stop, and same-bar duplicate protection.
6. Do not use getattr, file/network/database APIs, or unsafe imports.

When modifying an existing template:

Based on the current template, only adjust parameters and risk controls. Do not change the run-panel boundary.
Change take-profit to 0.8%, make inter-layer spacing grow with depth, and add a 12% hard stop.

19. Minimal Skeleton

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