Parameters

params is the dict on your indicator class that declares the user-facing inputs. Every key becomes a control in the side panel; every value defines its type, default, and bounds. You read the resolved values inside the indicator via ctx.params.<name>.

Two declaration forms

Dict shorthand (recommended)

Covers the common cases and is what the editor's default template uses:

class MyInd(Indicator):
    params = {
        "period": {"type": "int", "default": 14, "min": 2, "max": 200},
        "smooth": {"type": "bool", "default": True},
        "mode":   {"type": "str", "default": "fast", "choices": ["fast", "slow"]},
    }

Supported keys: type ("int", "float", "bool", "str"), default, min, max, choices, and description.

Param form

Use the Param dataclass when you need a source parameter (the kind modifier), or simply prefer the explicit form:

from indicator_api import Indicator, Param
class MyInd(Indicator):
    params = {
        "period": Param(type=int, default=14, min=2, max=200,
                        description="Window length"),
        "src":    Param(type=dict,
                        default={"type": "source", "value": "close"},
                        kind="source",
                        description="Bar series to run on"),
    }

The two forms are otherwise equivalent — mix them freely in one class.

⚠️ Source params must use Param(...). The dict shorthand does not carry the kind field — it's dropped during normalization. A source parameter declared with the dict form would be treated as a plain dict param and never resolve to a price series. Use Param(..., kind="source").

Reading parameters inside the indicator

Declared parameters arrive on ctx.params. Read them by attribute in init or on_bar:

class MyInd(Indicator):
    params = {
        "period": {"type": "int", "default": 14},
        "smooth": {"type": "bool", "default": True},
    }
    def init(self, ctx):
        self.ma = ctx.rolling_mean(source=ctx.close, period=ctx.params.period)
    def on_bar(self, ctx):
        value = self.ma.value
        if ctx.params.smooth and value is not None:
            ...
        ctx.plot.series("ma", value=value)

ctx.params is fully populated before init runs, so it's safe to size a helper from a parameter (period=ctx.params.period).

Types and the controls they get

type Notes
int Whole-number input. min / max bound it.
float Decimal input. min / max bound it.
bool Checkbox.
str Text input. With choices, a dropdown.
str + kind="color" A colour swatch with opacity (see below).
dict + kind="source" A bar-series picker (see below).

Colour parameters

Let the user pick your line colours instead of hard-coding them. Declare with Param(type=str, kind="color") and a hex default, then pass the value straight to color=:

from indicator_api import Indicator, Param
class MyMA(Indicator):
    params = {
        "period":   Param(type=int, default=20, min=2, max=500),
        "ma_color": Param(type=str, default="#fde68a", kind="color",
                          description="Line colour"),
    }
    def init(self, ctx):
        self.ma = ctx.rolling_mean(source=ctx.close, period=ctx.params.period)
    def on_bar(self, ctx):
        ctx.plot.series("ma", value=self.ma.value, color=ctx.params.ma_color)

Like source params, colour params need the Param(...) form — the dict shorthand has no kind.

Source parameters

A source parameter lets the user choose which price series your indicator runs on, so the same code works on close, hl2, volume, etc. without edits.

Declare it with Param(type=dict, kind="source", ...) and a default of {"type": "source", "value": "<name>"}, where <name> is one of:

open, high, low, close, volume, hl2, hlc3, hlcc4, ohlc4.

The runtime resolves the picked series before your code runs, so ctx.params.<name> is something you can feed straight into a helper's source=:

from indicator_api import Indicator, Param
class FlexibleMA(Indicator):
    name = "Flexible MA"
    params = {
        "period": Param(type=int, default=20, min=2, max=500),
        "src":    Param(type=dict,
                        default={"type": "source", "value": "close"},
                        kind="source",
                        description="Price series the average runs on"),
    }
    def init(self, ctx):
        self.ma = ctx.rolling_mean(source=ctx.params.src,
                                   period=ctx.params.period)
    def on_bar(self, ctx):
        ctx.plot.series("ma", value=self.ma.value, label="MA")

📌 Chained sources are not available yet. A second source shape — {"type": "source", "indicator_id": "...", "output": "..."}, feeding one indicator's output into another — is recognised but rejected with a clear "not yet supported" message. Use a built-in series (value) for now; chaining is planned for a follow-up.

Validation

After defaults are applied and values are type-coerced:

  • min and max are inclusive (min=2 means 2 is legal).
  • choices, when present, is enforced.
  • A value that can't be coerced to the declared type is rejected.

Any failure raises a ParamValidationError and the user sees an inline validation error in the panel rather than a silently-wrong chart.

Schema is re-derived from your code on save

Every save re-parses your source and rebuilds the parameter schema from scratch. Your code is the source of truth, not whatever the panel last showed:

  • Remove a param and it's gone.
  • Tighten min from 1 to 2 and the new bound applies; a chart sitting at value 1 will surface a validation error on its next run.
  • Rename a param and the old name is gone; existing charts fall back to the new param's default.

This is intentional — it keeps the saved indicator and its code in lockstep.

Documentation