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

QuantDinger Indicator Development Guide

Applies to: the current QuantDinger chart-indicator contract Audience: first-time indicator authors, developers migrating Pine/TDX formulas, and reviewers of AI-generated code

A QuantDinger indicator is a Python chart program that runs in the Indicator IDE. It reads the current K-line DataFrame, calculates aligned series, and returns plots, markers, and sparse chart layers through output.

The most important boundary is: an indicator is for chart analysis, not trade execution.

An indicator cannot place orders, backtest, run live trading, read an account, manage positions, enable leverage, or execute stop-loss/take-profit rules. To trade an idea, convert its visual signals into a Strategy API V2 strategy, then verify, backtest, and deploy that strategy separately.


1. Build a minimal indicator first

Paste this into the Indicator IDE and run it:

my_indicator_name = "Close Line"
my_indicator_description = "Displays the close price as a chart overlay."

df = df.copy()

close_line = [
    None if pd.isna(value) else float(value)
    for value in df["close"]
]

output = {
    "name": my_indicator_name,
    "plots": [
        {
            "name": "Close",
            "data": close_line,
            "color": "#3B82F6",
            "type": "line",
            "overlay": True,
        }
    ],
    "signals": [],
    "layers": [],
}

This is the complete minimal contract:

  1. Declare display metadata.
  2. Create a working copy with df = df.copy().
  3. calculate data aligned one-to-one with the K-line rows.
  4. Set the output dictionary.

For a new indicator, first draw one line, then add parameters, then event markers, and only then add advanced layers.


2. Indicator, strategy, and conversion boundaries

Artifact Owns Does not own
Chart Indicator plots, panes, lamp rows, visual markers, zones, labels backtest, live trading, orders, positions, leverage, execution risk
Strategy API V2 subscriptions, signals, order intents, positions, backtest, live execution, protections Indicator IDE output rendering
Indicator-to-Strategy translating visual signal meaning into executable code mixing order behavior into the source indicator

output["signals"] contains chart markers only. A sell-oriented “Death” marker may mean a long-exit warning or weakening conditions. It does not automatically open a short or reverse a position.

For a long-only conversion, the usual mapping is:

  • explicit bullish entry event → open_long
  • explicit bearish exit event → close_long
  • generate open_short only when the user explicitly requests shorting and provides a distinct bearish entry rule

Do not create execution columns such as open_long, close_long, open_short, close_short, add_long, or reduce_long in an indicator. Do not use legacy # @strategy annotations.


3. Runtime and input data

The runtime provides:

  • df: a pandas DataFrame for the current chart, ordered oldest to newest, one row per bar.
  • params: a dict produced by merging declared defaults with values from the parameter panel.
  • pd: preloaded pandas.
  • np: preloaded numpy.
  • open, high, low, close, and volume: some runtime entries also expose convenience Series. Prefer explicit df["close"] access for clarity and portability.

Standard OHLCV access:

open_price = df["open"]
high = df["high"]
low = df["low"]
close = df["close"]
volume = df["volume"]

Important rules:

  • Do not assume a time column exists; time may already be the DataFrame index.
  • Do not rename or remove required OHLCV columns.
  • Check before using optional fields, for example if "turnover" in df.columns:.
  • Run df = df.copy() before mutations.
  • Prefer vectorized rolling, ewm, shift, and where operations for core series.

# @param period int 20 Calculation period

my_indicator_name = "Example Indicator"
my_indicator_description = "Explains what is drawn and how events are marked."

df = df.copy()

period = int(params.get("period", 20))

# Helper functions
# Series calculation
# Marker construction

output = {
    "name": my_indicator_name,
    "plots": [],
    "signals": [],
    "layers": [],
}

Every indicator should declare:

my_indicator_name = "Dual EMA Viewer"
my_indicator_description = "Chart-only EMA crossover indicator with visual event markers."

Keep the name short and stable. The description should say:

  • what is calculated;
  • whether it appears on the price chart or a separate pane;
  • what each event marker means;
  • which parameters matter.

Do not promise returns or imply live validation.

Project source rules require English identifiers, metadata, comments, parameter descriptions, and default display labels. Localize display text only when a user explicitly requests a target language.


5. Declaring parameters

Syntax:

# @param <name> <int|float|bool|str> <default> <description>

Example:

# @param fast_len int 12 Fast EMA period
# @param slow_len int 26 Slow EMA period
# @param band_pct float 1.5 Channel width percent
# @param show_marks bool true Show crossover markers
# @param source str close Price source

A declaration does not create a Python variable. Read every value explicitly:

fast_len = int(params.get("fast_len", 12))
slow_len = int(params.get("slow_len", 26))
band_pct = float(params.get("band_pct", 1.5))
show_marks = bool(params.get("show_marks", True))
source = str(params.get("source", "close"))

Contract rules:

  • Declare one parameter per line.
  • Use valid Python identifiers for names.
  • Use only int, float, bool, str, or string.
  • The declared default must match the params.get fallback after type conversion.
  • Write booleans as true/false in declarations and True/False in Python.
  • A string default cannot contain spaces because it is parsed as one token.

Declare search candidates at the end of the description:

# @param period int 20 Lookback period range=5:60:5
# @param multiplier float 2.0 Band multiplier values=1.5,2.0,2.5,3.0

range=start:end:step produces an inclusive arithmetic sequence. values=a,b,c declares an explicit list. One parameter expands to at most 1,024 values. Range markers are removed from the human-facing description.

Indicator parameters control calculations and display only. Do not declare account, symbol, timeframe, position, leverage, stop-loss, or take-profit settings.


6. The output contract

The program must finish by setting a dict named output:

output = {
    "name": my_indicator_name,
    "plots": plots,
    "signals": signals,
    "layers": layers,
}

Optional:

output["calculatedVars"] = {}

Validation rules:

  • output must be a dict.
  • At least the plots or signals key must exist.
  • Every plot["data"] must have length len(df).
  • Every signal["data"] must have length len(df).
  • Do not emit NaN, positive infinity, or negative infinity; use None for missing points.
  • Layers do not need per-bar arrays, but indices, times, and prices must be meaningful for the current data.

Explicit empty lists are recommended:

output = {
    "name": my_indicator_name,
    "plots": [],
    "signals": [],
    "layers": [],
}

7. plots: price overlays and pane series

Each plot normally contains:

Field Type Meaning
name str legend and series name
data list values/None, length len(df)
color str preferably #RRGGBB
overlay bool True on price chart, False in a pane
type optional str commonly line; other current renderer styles may be used
plots = [
    {
        "name": "EMA Fast",
        "data": fast_values,
        "color": "#22C55E",
        "type": "line",
        "overlay": True,
    },
    {
        "name": "RSI",
        "data": rsi_values,
        "color": "#8B5CF6",
        "type": "line",
        "overlay": False,
    },
]

Moving averages, Bollinger bands, and price channels normally overlay the main chart. RSI, MACD, and lamp rows normally use separate panes.

Use one missing-value conversion helper:

def to_plot_list(series):
    return [
        None if pd.isna(value) else float(value)
        for value in series
    ]

Do not fill warm-up gaps in price overlays with zero. It draws misleading lines from zero to the actual market price.


8. signals: sparse visual events

signals = [
    {
        "type": "buy",
        "text": "Long Entry",
        "color": "#22C55E",
        "data": entry_marks,
    },
    {
        "type": "sell",
        "text": "Long Exit",
        "color": "#EF4444",
        "data": exit_marks,
    },
]

Rules:

  • type is commonly buy or sell. It controls marker orientation, not the signal name.
  • text is the stable name. Optional textData can provide a per-bar label.
  • Only a finite numeric data[i] activates a signal on bar i.
  • text and textData never activate a signal by themselves.
  • Empty positions must contain real None values.
  • Mark one-bar events by default instead of repeating a persistent state.

Convert a state into its rising edge:

def edge(condition):
    current = condition.fillna(False).astype(bool)
    previous = current.shift(1, fill_value=False).astype(bool)
    return current & ~previous

Build marker-price lists:

entry_event = edge(ema_fast > ema_slow)
exit_event = edge(ema_fast < ema_slow)

entry_marks = [
    float(df["low"].iloc[i] * 0.995)
    if bool(entry_event.iloc[i])
    else None
    for i in range(len(df))
]

exit_marks = [
    float(df["high"].iloc[i] * 1.005)
    if bool(exit_event.iloc[i])
    else None
    for i in range(len(df))
]

For “show on the next bar after confirmation”:

confirmed_entry = edge(raw_entry).shift(
    1,
    fill_value=False,
).astype(bool)

This moves a completed event later; it does not read future data.


9. layers: zones, lines, and labels

Prefer plots and signals for normal indicators. Add layers only for supply/demand zones, support/resistance, channels, invalidation levels, or structure labels that materially improve readability.

Zone:

{
    "type": "zone",
    "startIndex": 120,
    "endIndex": 180,
    "top": 105.2,
    "bottom": 101.8,
    "text": "Demand",
    "fillColor": "#22C55E",
    "borderColor": "#22C55E",
    "opacity": 0.12,
}

Horizontal line:

{
    "type": "line",
    "startIndex": 100,
    "endIndex": len(df) - 1,
    "price": 98.5,
    "text": "Support",
    "color": "#F59E0B",
    "dashed": True,
}

For a sloped line, replace price with startPrice and endPrice. Label:

{
    "type": "label",
    "index": len(df) - 1,
    "price": float(df["close"].iloc[-1]),
    "text": "Trend Weakens",
    "color": "#EF4444",
    "textColor": "#FFFFFF",
}

Indices are the most stable representation for the current df. Matching startTime, endTime, and time values are also supported.

Layers remain visual objects. They do not represent real orders, positions, or hosted stops.


10. pandas and numpy type traps

The most common failure is treating a numpy ndarray as a pandas Series.

Incorrect:

values = np.where(close > close.shift(1), close, 0)
average = values.rolling(10).mean()

np.where may return ndarray, which has no rolling, shift, ewm, fillna, or iloc.

Prefer pandas-native operations:

values = close.where(close > close.shift(1), 0)
average = values.rolling(10).mean()

When wrapping an ndarray is unavoidable:

array = np.where(close > close.shift(1), close, 0)
values = pd.Series(array, index=df.index)

Always pass index=df.index. Otherwise the new RangeIndex can silently misalign with a DatetimeIndex.

numpy form Preferred pandas form
np.where(cond, a, b) a.where(cond, b)
np.maximum(s, 0) s.clip(lower=0)
np.minimum(s, k) s.clip(upper=k)
np.abs(s) s.abs()

11. Avoid look-ahead and repainting

An indicator may use only the current and earlier bars. Do not use:

  • shift(-1) or shift(-N);
  • iloc[i + 1] inside row loops;
  • bars_ago(-N);
  • rolling(..., center=True);
  • the final full-dataset row to rewrite past signals;
  • future highs, lows, or confirmations to mark an earlier bar.

A valid crossover uses the current and previous state:

cross_up = (
    (ema_fast > ema_slow)
    & (ema_fast.shift(1) <= ema_slow.shift(1))
)

If a signal requires the current close to confirm, its converted strategy should execute later. Do not move the signal backward merely to improve the chart or backtest.


12. Sandbox and safety rules

Allowed computational modules include numpy, pandas, math, json, datetime, time, collections, functools, itertools, statistics, decimal, fractions, and copy. Since pd and np are preloaded, imports are normally unnecessary.

Do not use:

  • network, file, database, or subprocess access;
  • eval, exec, compile, or open;
  • reflection, dynamic imports, dunder escapes, or sandbox bypasses;
  • pandas/numpy file read, write, or serialization methods;
  • modules such as os, sys, requests, socket, subprocess, threading, sqlite3, pathlib, pickle, ctypes, or operator.

Validation has a timeout. Avoid unbounded loops, explosive recursion, and unnecessarily expensive row-by-row algorithms.


13. Complete tutorial: dual EMA viewer

# @param fast_len int 12 Fast EMA period range=5:30:1
# @param slow_len int 26 Slow EMA period range=10:80:2
# @param confirm_next_bar bool false Show markers one bar after confirmation
# @param show_marks bool true Show crossover markers

my_indicator_name = "Dual EMA Viewer"
my_indicator_description = "Displays two EMAs and marks confirmed crossover events."

df = df.copy()

fast_len = int(params.get("fast_len", 12))
slow_len = int(params.get("slow_len", 26))
confirm_next_bar = bool(params.get("confirm_next_bar", False))
show_marks = bool(params.get("show_marks", True))

close = df["close"]
high = df["high"]
low = df["low"]


def edge(condition):
    current = condition.fillna(False).astype(bool)
    previous = current.shift(1, fill_value=False).astype(bool)
    return current & ~previous


def to_plot_list(series):
    return [
        None if pd.isna(value) else float(value)
        for value in series
    ]


ema_fast = close.ewm(span=fast_len, adjust=False).mean()
ema_slow = close.ewm(span=slow_len, adjust=False).mean()

golden = edge(ema_fast > ema_slow)
death = edge(ema_fast < ema_slow)

if confirm_next_bar:
    golden = golden.shift(1, fill_value=False).astype(bool)
    death = death.shift(1, fill_value=False).astype(bool)

golden_marks = [
    float(low.iloc[i] * 0.995)
    if show_marks and bool(golden.iloc[i])
    else None
    for i in range(len(df))
]

death_marks = [
    float(high.iloc[i] * 1.005)
    if show_marks and bool(death.iloc[i])
    else None
    for i in range(len(df))
]

output = {
    "name": my_indicator_name,
    "plots": [
        {
            "name": "EMA Fast",
            "data": to_plot_list(ema_fast),
            "color": "#22C55E",
            "type": "line",
            "overlay": True,
        },
        {
            "name": "EMA Slow",
            "data": to_plot_list(ema_slow),
            "color": "#3B82F6",
            "type": "line",
            "overlay": True,
        },
    ],
    "signals": [
        {
            "type": "buy",
            "text": "Long Entry",
            "color": "#22C55E",
            "data": golden_marks,
        },
        {
            "type": "sell",
            "text": "Long Exit",
            "color": "#EF4444",
            "data": death_marks,
        },
    ],
    "layers": [],
}

How it works:

  1. Parameter declarations drive the panel and candidate ranges.
  2. params.get reads exactly matching defaults.
  3. EMAs are persistent state, so they belong in plots.
  4. Crossovers are one-time events, so edge converts them into signals.
  5. Empty marker slots contain None.
  6. The bearish marker is explicitly named “Long Exit,” preventing an accidental short-entry interpretation during conversion.

14. Validation, debugging, and common errors

Use this workflow after each meaningful change:

  1. Save a version.
  2. Run/preview the indicator.
  3. Execute code validation.
  4. Inspect warm-up bars, extreme markets, and short datasets.
  5. Change parameters and confirm that results respond.
  6. Confirm that signals appear only on event bars.
Hint or error Cause Fix
EMPTY_CODE no source provide complete indicator code
MISSING_OUTPUT no output assignment add the output dict
MissingOutput no output after execution check branches and scope
InvalidOutputType output is not a dict return a dict
InvalidOutputStructure neither plots nor signals exists provide at least one key
LengthMismatch data length differs from bars make every data list len(df)
MISSING_DF_COPY missing working copy add df = df.copy()
PARAM_DEFAULT_MISMATCH declaration and fallback differ align both defaults
DECLARED_PARAMS_NOT_READ_VIA_PARAMS_GET declared but not read call params.get
EXECUTION_COLUMNS_IGNORED_FOR_INDICATOR trade columns detected remove them and convert to V2
STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR legacy strategy annotations remove them
NDARRAY_PANDAS_METHOD_MISUSE ndarray used as Series use pandas or wrap with its index
FUTURE_DATA_LEAK future access detected use current and past data only

15. Semantic checklist before strategy conversion

Answer these questions before converting:

  • Which marker is the long entry?
  • Which marker is the long exit?
  • Is shorting genuinely required? A bearish long exit is not automatically a short entry.
  • Is reversal required? If so, are closing and reverse entry separate actions?
  • On which timeframe and completed bar is the signal confirmed?
  • Are repeated entries, scale-in, or scale-out allowed?
  • How should sizing, stop-loss, take-profit, and trailing protection work?
  • Which instrument and market type will the strategy own?

The converted strategy should remove chart-only colors, label offsets, plots, layers, and marker arrays. Preserve the signal algebra, then declare instruments, frequency, sizing, and risk explicitly with Strategy API V2.

Always verify and backtest generated strategy code again. Marketplace publication requires at least one successful backtest record.


16. Pre-publication checklist

  • [ ] Name and description exist and make no return claims.
  • [ ] Comments, identifiers, metadata, and default labels are English.
  • [ ] df = df.copy() is present.
  • [ ] Every parameter is read through params.get with a matching default.
  • [ ] No order, position, leverage, or execution-risk logic is present.
  • [ ] output is a dict.
  • [ ] Every plot/signal data list has length len(df).
  • [ ] Missing values are None; no NaN or infinity is emitted.
  • [ ] Signals are sparse events; persistent states use plots or a few layers.
  • [ ] No future data is accessed.
  • [ ] Numpy results are converted to indexed Series before pandas-only methods.
  • [ ] Short datasets, warm-up periods, and unusual parameters do not crash.
  • [ ] A version is saved and preview/validation passes.